diff --git a/markdoc/tags.js b/markdoc/tags.js index 22e3dbb14..4dd5389a9 100644 --- a/markdoc/tags.js +++ b/markdoc/tags.js @@ -11,6 +11,19 @@ import CalendlyEmbed from '@/components/CalendlyEmbed'; // Import the built-in Next.js tags import { comment } from '@markdoc/next.js/tags' +// Custom tag to allow embedding HTML elements +const UnescapedHtml = ({ htmlWrapperTag = 'div', children }) => { + const html = + typeof children === 'string' + ? children + : typeof children.props.children === 'string' + ? children.props.children + : children.props.children.join('') + + const CustomTag = htmlWrapperTag + return +} + const tags = { callout: { attributes: { @@ -82,6 +95,13 @@ const tags = { calendlyEmbed: { render: CalendlyEmbed, }, + html: { + render: UnescapedHtml, + attributes: { + htmlWrapperTag: { type: String }, + children: { type: String }, + }, + }, // tabs: { // render: Tabs, // attributes: {}, diff --git a/public/images/nft-create/pinata-key-two-permissions.png b/public/images/nft-create/pinata-key-two-permissions.png new file mode 100644 index 000000000..4d027399e Binary files /dev/null and b/public/images/nft-create/pinata-key-two-permissions.png differ diff --git a/public/images/nft-create/taquito-application-created-nft.png b/public/images/nft-create/taquito-application-created-nft.png new file mode 100644 index 000000000..b58d383ee Binary files /dev/null and b/public/images/nft-create/taquito-application-created-nft.png differ diff --git a/public/images/nft-create/taquito-application-flow.png b/public/images/nft-create/taquito-application-flow.png new file mode 100644 index 000000000..c18abe6af Binary files /dev/null and b/public/images/nft-create/taquito-application-flow.png differ diff --git a/public/images/nft-create/taquito-application-home.png b/public/images/nft-create/taquito-application-home.png new file mode 100644 index 000000000..298bd8f99 Binary files /dev/null and b/public/images/nft-create/taquito-application-home.png differ diff --git a/public/images/nft-create/web-ligo-ide-account.png b/public/images/nft-create/web-ligo-ide-account.png new file mode 100644 index 000000000..a42085ad7 Binary files /dev/null and b/public/images/nft-create/web-ligo-ide-account.png differ diff --git a/public/images/nft-create/web-ligo-ide-ghostnet.png b/public/images/nft-create/web-ligo-ide-ghostnet.png new file mode 100644 index 000000000..d9368eac8 Binary files /dev/null and b/public/images/nft-create/web-ligo-ide-ghostnet.png differ diff --git a/public/images/nft-pinata/image10.png b/public/images/nft-pinata/image10.png deleted file mode 100644 index c59d76225..000000000 Binary files a/public/images/nft-pinata/image10.png and /dev/null differ diff --git a/public/images/nft-pinata/image14.png b/public/images/nft-pinata/image14.png deleted file mode 100644 index 811e90e3e..000000000 Binary files a/public/images/nft-pinata/image14.png and /dev/null differ diff --git a/public/images/nft-pinata/image15.png b/public/images/nft-pinata/image15.png deleted file mode 100644 index 5cbb68ede..000000000 Binary files a/public/images/nft-pinata/image15.png and /dev/null differ diff --git a/public/images/nft-pinata/image19.png b/public/images/nft-pinata/image19.png deleted file mode 100644 index f377da6db..000000000 Binary files a/public/images/nft-pinata/image19.png and /dev/null differ diff --git a/public/images/nft-pinata/image20.png b/public/images/nft-pinata/image20.png deleted file mode 100644 index 765bcfa9e..000000000 Binary files a/public/images/nft-pinata/image20.png and /dev/null differ diff --git a/public/images/nft-pinata/image21.png b/public/images/nft-pinata/image21.png deleted file mode 100644 index d1d26f3c6..000000000 Binary files a/public/images/nft-pinata/image21.png and /dev/null differ diff --git a/public/images/nft-pinata/image22.png b/public/images/nft-pinata/image22.png deleted file mode 100644 index 894aab9fe..000000000 Binary files a/public/images/nft-pinata/image22.png and /dev/null differ diff --git a/public/images/nft-pinata/image23.png b/public/images/nft-pinata/image23.png deleted file mode 100644 index 1717c401c..000000000 Binary files a/public/images/nft-pinata/image23.png and /dev/null differ diff --git a/public/images/nft-pinata/image24.png b/public/images/nft-pinata/image24.png deleted file mode 100644 index 266f1b3ff..000000000 Binary files a/public/images/nft-pinata/image24.png and /dev/null differ diff --git a/public/images/nft-pinata/image26.png b/public/images/nft-pinata/image26.png deleted file mode 100644 index 62350a241..000000000 Binary files a/public/images/nft-pinata/image26.png and /dev/null differ diff --git a/public/images/nft-pinata/image28.png b/public/images/nft-pinata/image28.png deleted file mode 100644 index f972a2579..000000000 Binary files a/public/images/nft-pinata/image28.png and /dev/null differ diff --git a/public/images/nft-pinata/image29.png b/public/images/nft-pinata/image29.png deleted file mode 100644 index e0bc422cd..000000000 Binary files a/public/images/nft-pinata/image29.png and /dev/null differ diff --git a/public/images/nft-pinata/image32.png b/public/images/nft-pinata/image32.png deleted file mode 100644 index 766dc5736..000000000 Binary files a/public/images/nft-pinata/image32.png and /dev/null differ diff --git a/public/images/nft-pinata/image33.png b/public/images/nft-pinata/image33.png deleted file mode 100644 index 29c763bf0..000000000 Binary files a/public/images/nft-pinata/image33.png and /dev/null differ diff --git a/public/images/nft-pinata/image34.png b/public/images/nft-pinata/image34.png deleted file mode 100644 index 7a01b2728..000000000 Binary files a/public/images/nft-pinata/image34.png and /dev/null differ diff --git a/public/images/nft-pinata/image36.png b/public/images/nft-pinata/image36.png deleted file mode 100644 index bb5c9cdde..000000000 Binary files a/public/images/nft-pinata/image36.png and /dev/null differ diff --git a/public/images/nft-pinata/image37.png b/public/images/nft-pinata/image37.png deleted file mode 100644 index 7e63176b5..000000000 Binary files a/public/images/nft-pinata/image37.png and /dev/null differ diff --git a/public/images/nft-pinata/image41.png b/public/images/nft-pinata/image41.png deleted file mode 100644 index 805a256fc..000000000 Binary files a/public/images/nft-pinata/image41.png and /dev/null differ diff --git a/public/images/nft-pinata/image8.png b/public/images/nft-pinata/image8.png deleted file mode 100644 index 9a7004f53..000000000 Binary files a/public/images/nft-pinata/image8.png and /dev/null differ diff --git a/public/images/nft-pinata/image9.png b/public/images/nft-pinata/image9.png deleted file mode 100644 index 8fd278c79..000000000 Binary files a/public/images/nft-pinata/image9.png and /dev/null differ diff --git a/src/components/Layout.jsx b/src/components/Layout.jsx index 38e6c7814..5972eddf3 100644 --- a/src/components/Layout.jsx +++ b/src/components/Layout.jsx @@ -285,17 +285,17 @@ const tutorialNavigation = [ }, ], }, - { - title: 'Deploy your own smart rollup', - href: '/tutorials/smart-rollups', - }, { title: 'Create an NFT', href: '/tutorials/create-an-nft', children: [ { - title: 'Mint NFT using Taquito and Pinata', - href: '/tutorials/create-an-nft/nft-pinata', + title: 'Create an NFT with the `tznft` tool', + href: '/tutorials/create-an-nft/nft-tznft', + }, + { + title: 'Create a web app that mints NFTs', + href: '/tutorials/create-an-nft/nft-taquito', }, ], }, @@ -347,6 +347,36 @@ const tutorialNavigation = [ }, ], }, + { + title: 'Deploy a smart rollup', + href: '/tutorials/smart-rollups', + children: [ + { + title: "Introduction", + href: '/tutorials/smart-rollups', + }, + { + title: "Part 1: Setting up the application", + href: '/tutorials/smart-rollups/set-up', + }, + { + title: "Part 2: Running the kernel in debug mode", + href: '/tutorials/smart-rollups/debug', + }, + { + title: "Part 3: Optimizing the kernel", + href: '/tutorials/smart-rollups/optimize', + }, + { + title: "Part 4: Deploying (originating) the rollup", + href: '/tutorials/smart-rollups/deploy', + }, + { + title: "Part 5: Running and interacting with the rollup node", + href: '/tutorials/smart-rollups/run', + }, + ], + }, ], }, ] @@ -471,7 +501,7 @@ export function Layout({ children, title, tableOfContents, lastUpdated }) { let router = useRouter() let isHomePage = router.pathname === '/' - let tabPaths = ['tutorials', 'office-hours'] + let tabPaths = ['tutorials', 'office-hours', 'ethlondon'] let isTabHomePage = tabPaths.some((basePath) => router.pathname.endsWith(basePath)) || isHomePage diff --git a/src/components/TabLinks.jsx b/src/components/TabLinks.jsx index c11e594f9..224d54846 100644 --- a/src/components/TabLinks.jsx +++ b/src/components/TabLinks.jsx @@ -29,6 +29,7 @@ export function TabLinks({ isHomePage }) { className={`px-4 py-2 ${ !router.pathname.includes('tutorials') && !router.pathname.includes('office-hours') && + !router.pathname.includes('ethlondon') && !isHomePage ? 'text-blue-400' : 'text-white' @@ -89,6 +90,27 @@ export function TabLinks({ isHomePage }) { )} + + {isLargeScreen ? ( +
+ ETHLondon +
+ ) : ( +

+ ETHLondon +

+ )} + ) diff --git a/src/pages/ethlondon/index.md b/src/pages/ethlondon/index.md new file mode 100644 index 000000000..f9559f111 --- /dev/null +++ b/src/pages/ethlondon/index.md @@ -0,0 +1,77 @@ +--- +title: ETHLondon 2023 Cheatsheet +description: Get started on your Tezos hackathon project +--- + +Interested in building on Tezos for ETHLondon 2023? +**You’ve come to the right place.** + +## Support + +* We'll be at ETHLondon all weekend, look out for the people in white Tezos t-shirts! We're a friendly bunch, come and talk to us! +* Join our [Discord](https://discord.gg/tezos) and use the [#dev-help](https://discord.com/channels/699325006928281720/710095412639432705) channel to ask any questions +* You're on the docs site, so feel free to start at the [beginning](https://docs.tezos.com/tezos-basics/tezos-blockchain-overview/) or head straight over to the [tutorials](https://docs.tezos.com/tutorials/) to get started πŸš€ + + + +## Bounties + +{% table %} +* **ETHLondon Tezos Bounties** {% colspan=2 %} +--- +* **** +* **Prize Amount** +--- +* Best project using [SmartPy](https://smartpy.io) +* $2500 +--- +* Best project using [LIGO](https://ligolang.org/) +* $2500 +--- +* Best DeFi project +* $2500 +--- +* Best project using [Taquito](https://tezostaquito.io/) & [Beacon SDK](https://tezos.com/developers/docs/dapp-development/wallets-and-beacon-sdk/) +* $2500 + +{% /table %} + +## Quick Links + +{% table %} +* **Development** {% colspan=2 %} +--- +* **Languages** +* [SmartPy](https://smartpy.io/) +* [LIGO](https://ligolang.org/) +--- +* **Tooling** +* [Taqueria](https://taqueria.io/): `brew install taqueria` +* [Unity SDK (Gaming)](https://tezos.com/unity) +--- +* **Frontend** +* Looking for Ethers.JS? β€”> [Taquito](https://tezostaquito.io/) +* Looking for WalletConnect? β€”> [Beacon SDK](https://tezos.com/developers/docs/dapp-development/wallets-and-beacon-sdk/) +{% /table %} + +{% table %} + +* **Apps** {% colspan=2 %} +--- +* **Wallets** +* [Temple](https://templewallet.com/) +* [Umami](https://umamiwallet.com/) +* [Kukai](https://wallet.kukai.app/) +* [Airgap](https://airgap.it/) +--- +* **DeFi** +* [Plenty](https://plenty.network/) +* [Youves](https://youves.com/) +* [Quipuswap](https://quipuswap.com/) +--- +* **Art** +* [objkt](https://objkt.com/) +* [fxhash](https://www.fxhash.xyz/) +{% /table %} + +# Thanks and enjoy ETHLondon πŸ™ \ No newline at end of file diff --git a/src/pages/tutorials/build-an-nft-marketplace/index.md b/src/pages/tutorials/build-an-nft-marketplace/index.md index be87dde48..d01713ff1 100644 --- a/src/pages/tutorials/build-an-nft-marketplace/index.md +++ b/src/pages/tutorials/build-an-nft-marketplace/index.md @@ -1,130 +1,137 @@ --- id: build-an-nft-marketplace -title: Build an NFT Marketplace -lastUpdated: 10th July 2023 +title: NFT Marketplace Part 1 +lastUpdated: 11th October 2023 --- ## Introduction -Business objects managed by a blockchain are called `assets`. On Tezos you will find the term `Financial Asset or FA` with different versions 1, 2, or 2.1. +Welcome to the first part of our four-part series on building an NFT Marketplace. This tutorial aims to equip you with the knowledge and tools to create a robust NFT platform. -Here are different categorizations of assets. - -![](http://jingculturecommerce.com/wp-content/uploads/2021/03/nft-assets-1024x614.jpg) - -## Wine marketplace - -We are going to build a Wine marketplace extending the `@ligo/fa` package from the [Ligo repository](https://packages.ligolang.org/). The goal is to showcase how to extend an existing smart contract and build a frontend on top of it. - -The Wine marketplace is adding these features on top of a generic NFT contract : +In the first part, you learn: -- mint new wine bottles -- update wine bottle metadata details -- buy wine bottles -- sell wine bottles +- The concepts of FA, IPFS, and smart contracts. +- How to build an NFT Marketplace from the ligo/fa library. -You can play with the [final demo](https://demo.winefactory.marigold.dev/). - -![nftfactory.png](/images/nftfactory.png) +{% callout type="note" %} +This training course is provided by [Marigold](https://www.marigold.dev/). +You can find the 4 parts on github (solution + materials to build the UI) -{% callout type="note" %} -Here we present Part 1 of 4 of a training course by [Marigold](https://www.marigold.dev/). You can find all 4 parts on github. - [NFT 1](https://github.com/marigold-dev/training-nft-1): use FA2 NFT template to understand the basics - [NFT 2](https://github.com/marigold-dev/training-nft-2): finish FA2 NFT marketplace to introduce sales - [NFT 3](https://github.com/marigold-dev/training-nft-3): use FA2 single asset template to build another kind of marketplace - [NFT 4](https://github.com/marigold-dev/training-nft-4): use FA2 multi asset template to build last complex kind of marketplace -{% /callout %} +{% /callout %} +## Key Concepts -| Token template | # of token_type | # of item per token_type | -| -------------- | --------------- | ------------------------ | -| NFT | 0..n | 1 | -| single asset | 0..1 | 1..n | -| multi asset | 0..n | 1..n | +### What is FA? -{% callout type="note" %} -Because we are in web3, buy or sell features are a real payment system using on-chain XTZ tokens as money. This differs from traditional web2 applications where you have to integrate a payment system and so, pay extra fees -{% /callout %} +Business objects managed by a blockchain are called **assets**. On Tezos you find the term **Financial Asset or FA** with different versions 1, 2, or 2.1. -## Glossary +Here are different categorizations of assets. + +![](http://jingculturecommerce.com/wp-content/uploads/2021/03/nft-assets-1024x614.jpg) -## What is IPFS? +### What is IPFS? -The InterPlanetary File System is a protocol and peer-to-peer network for storing and sharing data in a distributed file system. IPFS uses content-addressing to uniquely identify each file in a global namespace connecting all computing devices. In this tutorial, we will be using [Pinata](https://www.pinata.cloud/) (free developer plan) to store the metadata for NFTs. An alternative would be to install a local IPFS node or an API gateway backend with a usage quota. +The InterPlanetary File System is a protocol and peer-to-peer network for storing and sharing data in a distributed file system. IPFS uses content-addressing to uniquely identify each file in a global namespace connecting all computing devices. This tutorial is using [Pinata](https://www.pinata.cloud/) (free developer plan) to store the metadata for NFTs. An alternative would be to install a local IPFS node or an API gateway backend with a usage quota. -## Smart Contracts +### Smart Contracts Overview -We will use two contracts for the marketplace. +There are two contracts for the marketplace. -### The token contract +#### 1. The token contract -On Tezos, FA2 is the standard for Non-Fungible Token contracts. We will be using the [template provided by Ligo](https://packages.ligolang.org/package/@ligo/fa) to build out the Token Contract. The template contains the basic entrypoints for building a Fungible or Non-fungible token including: +On Tezos, FA2 is the standard for Non-Fungible Token contracts. The [template provided by Ligo](https://packages.ligolang.org/package/@ligo/fa) will be used to build out the Token Contract. The template contains the basic entrypoints for building a Fungible or Non-fungible token including: - Transfer - Balance_of - Update_operators -### Marketplace unique contract +#### 2. Marketplace unique contract -On a second time, we will import the token contract into the marketplace unique contract. The latter will bring missing features as: +Next, you need to import the token contract into the marketplace unique contract. The latter is bringing missing features as: - Mint - Buy - Sell +## Wine marketplace + +After grasping the key concepts, we'll proceed to build a Wine marketplace extending the `@ligo/fa` package from the [Ligo repository](https://packages.ligolang.org/). The goal is to showcase how to extend an existing smart contract and build a frontend on top of it. + +The Wine marketplace is adding these features on top of a generic NFT contract : + +- mint new wine bottles +- update wine bottle metadata details +- buy wine bottles +- sell wine bottles + +You can play with the [final demo](https://demo.winefactory.marigold.dev/). + +![nftfactory.png](/images/nftfactory.png) + +| Token template | # of token_type | # of item per token_type | +| -------------- | --------------- | ------------------------ | +| NFT | 0..n | 1 | +| single asset | 0..1 | 1..n | +| multi asset | 0..n | 1..n | + +{% callout type="note" %} +Because of web3, buy or sell features are a real payment system using on-chain XTZ tokens as money. This differs from traditional web2 applications where you have to integrate a payment system and so, pay extra fees +{% /callout %} + ## Prerequisites -#### Required +Before building an NFT marketplace, ensure you have the following tools on hand. + +### Required - [npm](https://nodejs.org/en/download/): front-end is a TypeScript React client app -- [taqueria >= v0.28.5-rc](https://github.com/ecadlabs/taqueria) : Tezos app project tooling +- [taqueria >= v0.40.0](https://github.com/ecadlabs/taqueria) : Tezos app project tooling - [Docker](https://docs.docker.com/engine/install/): needed for `taqueria` - [jq](https://stedolan.github.io/jq/download/): extract `taqueria` JSON data -#### Recommended +### Recommended - [`VS Code`](https://code.visualstudio.com/download): as code editor - [`yarn`](https://classic.yarnpkg.com/lang/en/docs/install/#windows-stable): to build and run the front-end (see this article for more details about [differences between `npm` and `yarn`](https://www.geeksforgeeks.org/difference-between-npm-and-yarn/)) - [ligo VS Code extension](https://marketplace.visualstudio.com/items?itemName=ligolang-publish.ligo-vscode): for smart contract highlighting, completion, etc. - [Temple wallet](https://templewallet.com/): an easy to use Tezos wallet in your browser (or any other one with ghostnet support) -#### Optional +### Optional + - [taqueria VS Code extension](https://marketplace.visualstudio.com/items?itemName=ecadlabs.taqueria-vscode): visualize your project and execute tasks +## Smart Contract Modification -## Smart contract +Use **Taqueria** to shape the project structure, then create the NFT marketplace smart contract thanks to the `ligo/fa` library. -We will use `taqueria` to shape the project structure, then create the NFT marketplace smart contract thanks to the `ligo/fa` library. +{% callout type="note" %} +You require to copy some code from this git repository later, so you can clone it with: -{% callout type="note" %} -You will require to copy some code from this git repository later, so you can clone it with: +```bash +git clone https://github.com/marigold-dev/training-nft-1.git +``` - ```bash - git clone https://github.com/marigold-dev/training-nft-1.git - ``` {% /callout %} -### Taq'ify your project +### Step 1: Taq'ify your project + +First, set up our smart contract structure. ```bash taq init training cd training -taq install @taqueria/plugin-ligo@next -``` - -{% callout type="warning" %} -Important hack: create a dummy esy.json file with `{}` content on it. I will be used by the ligo package installer to not override the default package.json file of taqueria -{% /callout %} - -```bash -echo "{}" > esy.json +taq install @taqueria/plugin-ligo ``` **Your project is ready!** -### FA2 contract +### Step 2: FA2 contract -We will rely on the Ligo FA library. To understand in detail how assets work on Tezos, please read below notes: +Next, you need to build the FA2 contract which relies on the Ligo FA library. To understand in detail how assets work on Tezos, please read the notes below. - [FA2 standard](https://gitlab.com/tezos/tzip/-/blob/master/proposals/tzip-12/tzip-12.md) @@ -135,12 +142,13 @@ We will rely on the Ligo FA library. To understand in detail how assets work on Install the `ligo/fa` library locally: ```bash -TAQ_LIGO_IMAGE=ligolang/ligo:0.63.2 taq ligo --command "install @ligo/fa" +echo '{ "name": "app", "dependencies": { "@ligo/fa": "^1.0.8" } }' >> ligo.json +TAQ_LIGO_IMAGE=ligolang/ligo:1.0.0 taq ligo --command "install @ligo/fa" ``` -### NFT marketplace contract +### Step 3: NFT marketplace contract -Create the NFT marketplace contract with `taqueria` +Then, create the NFT marketplace contract with `taqueria` ```bash taq create contract nft.jsligo @@ -149,7 +157,7 @@ taq create contract nft.jsligo Remove the default code and paste this code instead ```ligolang -#import "@ligo/fa/lib/fa2/nft/NFT.jsligo" "NFT" +#import "@ligo/fa/lib/fa2/nft/nft.impl.jsligo" "FA2Impl" /* ERROR MAP FOR UI DISPLAY or TESTS const errorMap : map = Map.literal(list([ @@ -163,109 +171,193 @@ Remove the default code and paste this code instead ])); */ -type storage = - { - administrators: set
, - ledger: NFT.Ledger.t, - metadata: NFT.Metadata.t, - token_metadata: NFT.TokenMetadata.t, - operators: NFT.Operators.t, - token_ids : set - }; +export type storage = { + administrators: set
, + ledger: FA2Impl.NFT.ledger, + metadata: FA2Impl.TZIP16.metadata, + token_metadata: FA2Impl.TZIP12.tokenMetadata, + operators: FA2Impl.NFT.operators +}; type ret = [list, storage]; - -type parameter = - | ["Mint", nat,bytes,bytes,bytes,bytes] //token_id, name , description ,symbol , ipfsUrl - | ["AddAdministrator" , address] - | ["Transfer", NFT.transfer] - | ["Balance_of", NFT.balance_of] - | ["Update_operators", NFT.update_operators]; - - -const main = ([p, s]: [parameter,storage]): ret => - match(p, { - Mint: (p: [nat,bytes,bytes,bytes,bytes]) => [list([]),s], - AddAdministrator : (p : address) => {if(Set.mem(Tezos.get_sender(), s.administrators)){ return [list([]),{...s,administrators:Set.add(p, s.administrators)}]} else {return failwith("1");}} , - Transfer: (p: NFT.transfer) => [list([]),s], - Balance_of: (p: NFT.balance_of) => [list([]),s], - Update_operators: (p: NFT.update_operator) => [list([]),s], - }); ``` Explanations: -- the first line `#import "@ligo/fa/lib/fa2/nft/NFT.jsligo" "NFT"` imports the Ligo FA library that we are going to extend. We will add new entrypoints the the base code. -- `storage` definition is an extension of the imported library storage, we point to the original types keeping the same naming - - `NFT.Ledger.t` : keep/trace ownership of tokens - - `NFT.Metadata.t` : tzip-16 compliance - - `NFT.TokenMetadata.t` : tzip-12 compliance - - `NFT.Operators.t` : permissions part of FA2 standard - - `NFT.Storage.token_id>` : cache for keys of token_id bigmap +- the first line `#import "@ligo/fa/lib/fa2/nft/nft.impl.jsligo" "FA2Impl"` imports the Ligo FA library implementation that your code is extending. Then, add new entrypoints to the base code. +- `storage` definition is an extension of the imported library storage. You need to point to the original types keeping the same naming + - `FA2Impl.NFT.ledger` : keep/trace ownership of tokens + - `FA2Impl.TZIP16.metadata` : tzip-16 compliance + - `FA2Impl.TZIP12.tokenMetadata` : tzip-12 compliance + - `FA2Impl.NFT.operators` : permissions part of FA2 standard - `storage` has more fields to support a set of `administrators` -- `parameter` definition is an extension of the imported library entrypoints - - `NFT.transfer` : to transfer NFTs - - `NFT.balance_of` : to check token balance for a specific user (on this template it will return always 1) - - `NFT.update_operators` : to allow other users to manage our NFT -- `parameter` has more entrypoints to allow to create NFTs `Mint` -- `parameter` has an entrypoint `AddAdministrator` to add new administrators. Administrators will be allowed to mint NFTs - -Compile the contract -```bash -TAQ_LIGO_IMAGE=ligolang/ligo:0.60.0 taq compile nft.jsligo -``` +The contract compiles, now let's write `transfer,balance_of,update_operators` entrypoints. You do a passthrough call to the underlying library. -{% callout type="note" %} -To be sure that Taqueria will use a correct version of Ligo containing the Ligo package installer w/ Docker fix, we set the env var `TAQ_LIGO_IMAGE` -{% /callout %} +```ligolang +@entry +const transfer = (p: FA2Impl.TZIP12.transfer, s: storage): ret => { + const ret2: [list, FA2Impl.NFT.storage] = + FA2Impl.NFT.transfer( + p, + { + ledger: s.ledger, + metadata: s.metadata, + token_metadata: s.token_metadata, + operators: s.operators, + } + ); + return [ + ret2[0], + { + ...s, + ledger: ret2[1].ledger, + metadata: ret2[1].metadata, + token_metadata: ret2[1].token_metadata, + operators: ret2[1].operators, + } + ] +}; -The contract compiles, now let's write `Transfer,Balance_of,Update_operators` entrypoints. We will do a passthrough call to the underlying library. On `main` function, **replace the default cases code with this one** +@entry +const balance_of = (p: FA2Impl.TZIP12.balance_of, s: storage): ret => { + const ret2: [list, FA2Impl.NFT.storage] = + FA2Impl.NFT.balance_of( + p, + { + ledger: s.ledger, + metadata: s.metadata, + token_metadata: s.token_metadata, + operators: s.operators, + } + ); + return [ + ret2[0], + { + ...s, + ledger: ret2[1].ledger, + metadata: ret2[1].metadata, + token_metadata: ret2[1].token_metadata, + operators: ret2[1].operators, + } + ] +}; -```ligolang - Transfer: (p: NFT.transfer) => { - const ret2 : [list, NFT.storage] = NFT.transfer(p,{ledger:s.ledger,metadata:s.metadata,token_metadata:s.token_metadata,operators:s.operators,token_ids : s.token_ids}); - return [ret2[0],{...s,ledger:ret2[1].ledger,metadata:ret2[1].metadata,token_metadata:ret2[1].token_metadata,operators:ret2[1].operators,token_ids:ret2[1].token_ids}]; - }, - Balance_of: (p: NFT.balance_of) => { - const ret2 : [list, NFT.storage] = NFT.balance_of(p,{ledger:s.ledger,metadata:s.metadata,token_metadata:s.token_metadata,operators:s.operators,token_ids : s.token_ids}); - return [ret2[0],{...s,ledger:ret2[1].ledger,metadata:ret2[1].metadata,token_metadata:ret2[1].token_metadata,operators:ret2[1].operators,token_ids:ret2[1].token_ids}]; - }, - Update_operators: (p: NFT.update_operator) => { - const ret2 : [list, NFT.storage] = NFT.update_ops(p,{ledger:s.ledger,metadata:s.metadata,token_metadata:s.token_metadata,operators:s.operators,token_ids : s.token_ids}); - return [ret2[0],{...s,ledger:ret2[1].ledger,metadata:ret2[1].metadata,token_metadata:ret2[1].token_metadata,operators:ret2[1].operators,token_ids:ret2[1].token_ids}]; +@entry +const update_operators = (p: FA2Impl.TZIP12.update_operators, s: storage): ret => { + const ret2: [list, FA2Impl.NFT.storage] = + FA2Impl.NFT.update_operators( + p, + { + ledger: s.ledger, + metadata: s.metadata, + token_metadata: s.token_metadata, + operators: s.operators, } + ); + return [ + ret2[0], + { + ...s, + ledger: ret2[1].ledger, + metadata: ret2[1].metadata, + token_metadata: ret2[1].token_metadata, + operators: ret2[1].operators, + } + ] +}; ``` -Explanations: +Explanation: -- every NFT.xxx() called function is taking the storage type of the NFT library, so we send a partial object from our storage definition to match the type definition -- the return type contains also the storage type of the library, so we need to reconstruct the storage by copying the modified fields +- every `FA2Impl.NFT.xxx()` called function is taking the storage type of the NFT library, so you need to send a partial object from our storage definition to match the type definition +- the return type contains also the storage type of the library, so you need to reconstruct the storage by copying the modified fields {% callout type="note" %} -The LIGO team is working on merging type definitions, so you then can do `type union` or `merge 2 objects` like in Typescript +The LIGO team is working on merging type definitions, so you then can do **type union** or **merge 2 objects** like in Typescript {% /callout %} -Let's add the `Mint` function now. Add the new function, and update the main function +Let's add the `Mint` function now. Add the new function ```ligolang -const mint = (token_id : nat, name :bytes, description:bytes ,symbol :bytes, ipfsUrl:bytes, s: storage) : ret => { +@entry +const mint = ( + [token_id, name, description, symbol, ipfsUrl]: [ + nat, + bytes, + bytes, + bytes, + bytes + ], + s: storage +): ret => { + if (! Set.mem(Tezos.get_sender(), s.administrators)) return failwith("1"); + const token_info: map = + Map.literal( + list( + [ + ["name", name], + ["description", description], + ["interfaces", (bytes `["TZIP-12"]`)], + ["artifactUri", ipfsUrl], + ["displayUri", ipfsUrl], + ["thumbnailUri", ipfsUrl], + ["symbol", symbol], + ["decimals", (bytes `0`)] + ] + ) + ) as map; + return [ + list([]) as list, + { + ...s, + ledger: Big_map.add(token_id, Tezos.get_sender(), s.ledger) as + FA2Impl.NFT.ledger, + token_metadata: Big_map.add( + token_id, + { token_id: token_id, token_info: token_info }, + s.token_metadata + ), + operators: Big_map.empty as FA2Impl.NFT.operators, + } + ] +}; +``` - if(! Set.mem(Tezos.get_sender(), s.administrators)) return failwith("1"); +Explanation: - const token_info: map = - Map.literal(list([ - ["name", name], - ["description",description], - ["interfaces", (bytes `["TZIP-12"]`)], - ["thumbnailUri", ipfsUrl], - ["symbol",symbol], - ["decimals", (bytes `0`)] - ])) as map; +- `mint` function allows you to create a unique NFT. You have to declare the name, description, symbol, and ipfsUrl for the picture to display +- to simplify, the code here does not manage the increment of the token_id here it is done by the front end later. You should manage this counter on-chain to avoid overriding an existing NFT. There is no rule to allocate a specific number to the token_id but people increment it from 0. Also, there is no rule if you have a burn function to reallocate the token_id to a removed index and just continue the sequence from the greatest index. +- most of the fields are optional except `decimals` that is set to `0`. A unique NFT does not have decimals, it is a unit +- by default, the `quantity` for an NFT is `1`, that is why every bottle is unique and there is no need to set a total supply on each NFT. +- if you want to know the `size of the NFT collection`, you need an indexer on the frontend side. It is not possible to have this information on the contract (because big_map has not a .keys() function returning the keys) unless you add and additional element on the storage to cache it +Smart contract implementation for this first training is finished, let's prepare the deployment to ghostnet. - const metadata : bytes = bytes - `{ +Compile the file to create a default taqueria initial storage and parameter file + +```bash +TAQ_LIGO_IMAGE=ligolang/ligo:1.0.0 taq compile nft.jsligo +``` + +Edit the new storage file `nft.storageList.jsligo` as it. (:warning: you can change the `administrator` address to your own address or keep alice address `tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb`) + +```ligolang +#import "nft.jsligo" "Contract" + +const default_storage : Contract.storage = { + administrators: Set.literal( + list(["tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb" as address]) + ) as set
, + ledger: Big_map.empty as Contract.FA2Impl.NFT.ledger, + metadata: Big_map.literal( + list( + [ + ["", bytes `tezos-storage:data`], + [ + "data", + bytes + `{ "name":"FA2 NFT Marketplace", "description":"Example of FA2 implementation", "version":"0.0.1", @@ -278,63 +370,26 @@ const mint = (token_id : nat, name :bytes, description:bytes ,symbol :bytes, ipf "interfaces":["TZIP-012"], "errors": [], "views": [] - }` ; - - return [list([]) as list, - {...s, - ledger: Big_map.add(token_id,Tezos.get_sender(),s.ledger) as NFT.Ledger.t, - metadata : Big_map.literal(list([["", bytes `tezos-storage:data`],["data", metadata]])), - token_metadata: Big_map.add(token_id, {token_id: token_id,token_info:token_info},s.token_metadata), - operators: Big_map.empty as NFT.Operators.t, - token_ids : Set.add(token_id,s.token_ids) - }]}; - -const main = ([p, s]: [parameter,storage]): ret => - match(p, { - Mint: (p: [nat,bytes,bytes,bytes,bytes]) => mint(p[0],p[1],p[2],p[3],p[4],s), - AddAdministrator : (p : address) => {if(Set.mem(Tezos.get_sender(), s.administrators)){ return [list([]),{...s,administrators:Set.add(p, s.administrators)}]} else {return failwith("1");}} , - Transfer: (p: NFT.transfer) => [list([]),s], - Balance_of: (p: NFT.balance_of) => [list([]),s], - Update_operators: (p: NFT.update_operator) => [list([]),s], - }); -``` - -Explanations: - -- `mint` function will allow you to create a unique NFT. You have to declare the name, description, symbol, and ipfsUrl for the picture to display -- to simplify, we don't manage the increment of the token_id here it will be done by the front end later. We encourage you to manage this counter on-chain to avoid overriding an existing NFT. There is no rule to allocate a specific number to the token_id but people increment it from 0. Also, there is no rule if you have a burn function to reallocate the token_id to a removed index and just continue the sequence from the greatest index. -- most of the fields are optional except `decimals` that is set to `0`. A unique NFT does not have decimals, it is a unit -- by default, the `quantity` for an NFT is `1`, that is why every bottle is unique and we don't need to set a total supply on each NFT. -- if you want to know the `size of the NFT collection`, look at `token_ids` size. This is used as a `cache` key index of the `token_metadata` big_map. By definition, a big map in Tezos can be accessed through a key, but you need to know the key, there is no function to return the keyset. This is why we keep a trace of all token_id in this set, so we can loop and read/update information on NFTs - -We have finished the smart contract implementation for this first training, let's prepare the deployment to ghostnet. - -Edit the storage file `nft.storageList.jsligo` as it. (:warning: you can change the `administrator` address to your own address or keep `alice`) - -```ligolang -#include "nft.jsligo" -const default_storage = - {administrators: Set.literal(list(["tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb" - as address])) - as set
, - ledger: Big_map.empty as NFT.Ledger.t, - metadata: Big_map.empty as NFT.Metadata.t, - token_metadata: Big_map.empty as NFT.TokenMetadata.t, - operators: Big_map.empty as NFT.Operators.t, - token_ids: Set.empty as set - }; + }` + ] + ] + ) + ) as Contract.FA2Impl.TZIP16.metadata, + token_metadata: Big_map.empty as Contract.FA2Impl.TZIP12.tokenMetadata, + operators: Big_map.empty as Contract.FA2Impl.NFT.operators, +}; ``` -Compile again and deploy to ghostnet +Compile and deploy to ghostnet ```bash -TAQ_LIGO_IMAGE=ligolang/ligo:0.60.0 taq compile nft.jsligo -taq install @taqueria/plugin-taquito@next +TAQ_LIGO_IMAGE=ligolang/ligo:1.0.0 taq compile nft.jsligo +taq install @taqueria/plugin-taquito taq deploy nft.tz -e "testing" ``` {% callout type="note" %} -If this is the first time you're using `taqueria`, you may want to run through [this training](https://github.com/marigold-dev/training-dapp-1#ghostnet-testnet-wallet). +If this is the first time you're using **taqueria**, you may want to run through [this training](https://github.com/marigold-dev/training-dapp-1#ghostnet-testnet-wallet). {% /callout %} > For advanced users, just go to `.taq/config.local.testing.json` and change the default account by alice one's (publicKey,publicKeyHash,privateKey) and then redeploy: @@ -362,17 +417,19 @@ taq deploy nft.tz -e "testing" β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Contract β”‚ Address β”‚ Alias β”‚ Balance In Mutez β”‚ Destination β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ nft.tz β”‚ KT1PLo2zWETRkmqUFEiGqQNVUPorWHVHgHMi β”‚ nft β”‚ 0 β”‚ https://ghostnet.ecadinfra.com β”‚ +β”‚ nft.tz β”‚ KT18sgGX5nu4BzwV2JtpQy4KCqc8cZU5MwnN β”‚ nft β”‚ 0 β”‚ https://ghostnet.ecadinfra.com β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` -** We have finished the backend! ** +**Backend is finished!** ## NFT Marketplace frontend -## Get the react boilerplate +This section guides you step-by-step in setting up an intuitive frontend. -To save time, we have a [boilerplate ready for the UI](https://github.com/marigold-dev/training-nft-1/tree/main/reactboilerplateapp) +### Step 1: Get the react boilerplate + +To save time, a [boilerplate ready for the UI](https://github.com/marigold-dev/training-nft-1/tree/main/reactboilerplateapp) is ready for you. Copy this code into your folder (:warning: assuming you have cloned this repo and your current path is `$REPO/training`) @@ -389,33 +446,33 @@ Install the plugin, then generate a representation of your smart contract object Finally, run the server ```bash -taq install @taqueria/plugin-contract-types@next +taq install @taqueria/plugin-contract-types taq generate types ./app/src cd app yarn install -yarn run start +yarn dev ``` -> Note : On `Mac` :green_apple:, `sed` does not work as Unix, change the start script on package.json to -> ` "start": "if test -f .env; then sed -i '' \"s/\\(REACT_APP_CONTRACT_ADDRESS *= *\\).*/\\1$(jq -r 'last(.tasks[]).output[0].address' ../.taq/testing-state.json)/\" .env ; else jq -r '\"REACT_APP_CONTRACT_ADDRESS=\" + last(.tasks[]).output[0].address' ../.taq/testing-state.json > .env ; fi && react-app-rewired start",` +> Note : On a **Mac** :green_apple:, sometimes `sed` commands do not work exactly the same as Unix commands. Look at the start script on package.json for Mac below : +> ` "dev": "if test -f .env; then sed -i '' \"s/\\(VITE_CONTRACT_ADDRESS *= *\\).*/\\1$(jq -r 'last(.tasks[]).output[0].address' ../.taq/testing-state.json)/\" .env ; else jq -r '\"VITE_CONTRACT_ADDRESS=\" + last(.tasks[]).output[0].address' ../.taq/testing-state.json > .env ; fi && vite",` The website is ready! You have: -- automatic pull from `taqueria` last deployed contract address at each start +- last deployed contract address always is refreshed from **taqueria** configuration at each start - login/logout - the general layout / navigation If you try to connect you are redirected to `/` path that is also the wine catalog. -There are no bottle collections yet, so we need to create the mint page. +There are no bottle collections yet, so you have to create the mint page. -## Mint Page +### Step 2: Mint Page -Edit default Mint Page on `./src/MintPage.tsx` +Edit default mint Page on `./src/MintPage.tsx` -### Add a form to create the NFT +#### Add a form to create the NFT -In `MintPage.tsx`, replace the `HTML` template with this one : +In `MintPage.tsx`, replace the **HTML** template starting with `` with this one : ```html @@ -572,7 +629,9 @@ In `MintPage.tsx`, replace the `HTML` template with this one : ``` -Add `formik` form to your Component function inside the same `MintPage.tsx` file: +Inside your `MintPage` Component function, all all following elements : + +- A `formik` form : ```typescript const validationSchema = yup.object({ @@ -627,7 +686,7 @@ const toggleDrawer = }; ``` -Finally, fix the missing imports: +Finally, fix the missing imports at the beginning of the file : ```typescript import { AddCircleOutlined, Close } from "@mui/icons-material"; @@ -650,9 +709,9 @@ import { TZIP21TokenMetadata, UserContext, UserContextType } from "./App"; import { address } from "./type-aliases"; ``` -### Add mint missing function +#### Add mint missing function -Add the `mint` function and related imports : +First, add the `mint` function and related imports : ```typescript import { useSnackbar } from "notistack"; @@ -662,6 +721,8 @@ import { char2Bytes } from "@taquito/utils"; import { TransactionInvalidBeaconError } from "./TransactionInvalidBeaconError"; ``` +Add the `mint` function inside your `MintPage` Component function + ```typescript const { enqueueSnackbar } = useSnackbar(); @@ -675,11 +736,11 @@ const mint = async (newTokenDefinition: TZIP21TokenMetadata) => { const requestHeaders: HeadersInit = new Headers(); requestHeaders.set( "pinata_api_key", - `${process.env.REACT_APP_PINATA_API_KEY}` + `${import.meta.env.VITE_PINATA_API_KEY}` ); requestHeaders.set( "pinata_secret_api_key", - `${process.env.REACT_APP_PINATA_API_SECRET}` + `${import.meta.env.VITE_PINATA_API_SECRET}` ); const resFile = await fetch( @@ -735,28 +796,30 @@ const mint = async (newTokenDefinition: TZIP21TokenMetadata) => { }; ``` +> Note : organize/fix duplicated import declarations if necessary + ![mint form](/images/mintForm.png) Explanations: -- on Mint button click, we upload a file and then we call the `pinata API` to push the file to `IPFS`. It returns the hash +- on Mint button click, upload a file and then call the **pinata API** to push the file to **IPFS**. It returns the hash - hash is used in two different ways - https pinata gateway link (or any other ipfs http viewer) - ipfs link for the backend thumbnail url -- TZIP standard requires storing data in `bytes`. As there is no Michelson function to convert string to bytes (using Micheline data PACK will not work as it alters the final bytes), we do the conversion using `char2Bytes` on the frontend side +- TZIP standard requires storing data in `bytes`. As there is no Michelson function to convert string to bytes (using Micheline data PACK is not working, as it alters the final bytes), do the conversion using `char2Bytes` on the frontend side -> Note : Finally, if you remember on the backend , we said that token_id increment management was done in the ui, so you can write this code. It is not a good security practice as it supposes that the counter is managed on frontend side, but it is ok for demo purpose. +> Note : Finally, if you remember on the backend, token_id increment management was done in the ui, so you can write this code. It is not a good security practice as it supposes that the counter is managed on frontend side, but it is ok for demo purpose. -Add this code, every time you have a new token minted, you increment the counter for the next one +Add this code inside your `MintPage` Component function , every time you have a new token minted, you increment the counter for the next one ```typescript useEffect(() => { (async () => { - if (storage && storage.token_ids.length > 0) { - formik.setFieldValue("token_id", storage?.token_ids.length); + if (nftContratTokenMetadataMap && nftContratTokenMetadataMap.size > 0) { + formik.setFieldValue("token_id", nftContratTokenMetadataMap.size); } })(); -}, [storage?.token_ids]); +}, [nftContratTokenMetadataMap?.size]); ``` ### Display all minted bottles @@ -850,7 +913,7 @@ Replace the `"//TODO"` keyword with this template ``` -Add missing imports and parameters +Finally, your imports at beginning of the file should be like this : ```typescript import SwipeableViews from "react-swipeable-views"; @@ -875,8 +938,21 @@ import { KeyboardArrowLeft, KeyboardArrowRight, } from "@mui/icons-material"; +import Paper from "@mui/material/Paper"; +import Typography from "@mui/material/Typography"; +import { useFormik } from "formik"; +import React, { useEffect, useState } from "react"; +import * as yup from "yup"; +import { TZIP21TokenMetadata, UserContext, UserContextType } from "./App"; +import { useSnackbar } from "notistack"; +import { BigNumber } from "bignumber.js"; +import { address, bytes, nat } from "./type-aliases"; +import { char2Bytes } from "@taquito/utils"; +import { TransactionInvalidBeaconError } from "./TransactionInvalidBeaconError"; ``` +and some variables inside your `MintPage` Component function + ```typescript const [activeStep, setActiveStep] = React.useState(0); @@ -895,30 +971,28 @@ const handleStepChange = (step: number) => { ## Let's play -1. Connect with your wallet and choose `alice` account _(or the administrator you set on the smart contract earlier)_. You are redirected to the Administration /mint page as there is no NFT minted yet. - -2. Create your first wine bottle, for example: - -- `name`: Saint Emilion - Franc la Rose -- `symbol`: SEMIL -- `description`: Grand cru 2007 - -3. Click on `Upload an image` and select a bottle picture on your computer - -4. Click on the Mint button +- Connect with your wallet and choose **alice** account _(or the administrator you set on the smart contract earlier)_. You are redirected to the Administration /mint page as there is no NFT minted yet. +- Create your first wine bottle, for example: + - `name`: Saint Emilion - Franc la Rose + - `symbol`: SEMIL + - `description`: Grand cru 2007 +- Click on **Upload an image** and select a bottle picture on your computer +- Click on the Mint button ![minting](/images/minting.png) -Your picture will be pushed to IPFS and displayed. +Your picture is be pushed to IPFS and displayed. -Then, Temple Wallet _(or whatever other wallet you choose)_ will ask you to sign the operation. Confirm it, and less than 1 minute after the confirmation notification, the page will be automatically refreshed to display your wine collection with your first NFT! +Then, Temple Wallet _(or whatever other wallet you choose)_ asks you to sign the operation. Confirm it, and less than 1 minute after the confirmation notification, the page is automatically refreshed to display your wine collection with your first NFT! Now you can see all NFTs ![wine collection](/images/winecollection.png) -## Conclusion +## Summary You are able to create an NFT collection marketplace from the `ligo/fa` library. +On next training, you will add the buy and sell functions to your smart contract and update the frontend to allow these actions. + To continue, let's go to [Part 2](/tutorials/build-an-nft-marketplace/part-2). diff --git a/src/pages/tutorials/build-an-nft-marketplace/part-2.md b/src/pages/tutorials/build-an-nft-marketplace/part-2.md index 8bddca9a4..5ab5dd420 100644 --- a/src/pages/tutorials/build-an-nft-marketplace/part-2.md +++ b/src/pages/tutorials/build-an-nft-marketplace/part-2.md @@ -1,10 +1,14 @@ --- -id: nft-marketplace-part-2 +id: build-an-nft-marketplace title: NFT Marketplace Part 2 -lastUpdated: 7th July 2023 +lastUpdated: 11th October 2023 --- -This time we will add the ability to buy and sell an NFT! +## Introduction + +![https://img.etimg.com/thumb/msid-71286763,width-1070,height-580,overlay-economictimes/photo.jpg](https://img.etimg.com/thumb/msid-71286763,width-1070,height-580,overlay-economictimes/photo.jpg) + +This time, buy and sell an NFT feature is added ! Keep your code from the previous lesson or get the solution [here](https://github.com/marigold-dev/training-nft-1/tree/main/solution) @@ -24,153 +28,75 @@ Add the following code sections on your `nft.jsligo` smart contract Add offer type ```ligolang -type offer = { +export type offer = { owner : address, price : nat }; ``` -Add `offers` field to storage - -```ligolang -type storage = - { - administrators: set
, - offers: map, //user sells an offer - ledger: NFT.Ledger.t, - metadata: NFT.Metadata.t, - token_metadata: NFT.TokenMetadata.t, - operators: NFT.Operators.t, - token_ids : set - }; -``` - -Add 2 variants `Buy` and `Sell` to the parameter +Add `offers` field to storage, it should look like this below : ```ligolang -type parameter = - | ["Mint", nat,bytes,bytes,bytes,bytes] //token_id, name , description ,symbol , ipfsUrl - | ["Buy", nat, address] //buy token_id at a seller offer price - | ["Sell", nat, nat] //sell token_id at a price - | ["AddAdministrator" , address] - | ["Transfer", NFT.transfer] - | ["Balance_of", NFT.balance_of] - | ["Update_operators", NFT.update_operators]; -``` - -Add 2 entrypoints `Buy` and `Sell` inside the `main` function - -```ligolang -const main = ([p, s]: [parameter,storage]): ret => - match(p, { - Mint: (p: [nat,bytes,bytes,bytes,bytes]) => mint(p[0],p[1],p[2],p[3],p[4],s), - Buy: (p : [nat,address]) => [list([]),s], - Sell: (p : [nat,nat]) => [list([]),s], - AddAdministrator : (p : address) => {if(Set.mem(Tezos.get_sender(), s.administrators)){ return [list([]),{...s,administrators:Set.add(p, s.administrators)}]} else {return failwith("1");}} , - Transfer: (p: NFT.transfer) => { - const ret2: [list, NFT.storage] = - NFT.transfer( - p, - { - ledger: s.ledger, - metadata: s.metadata, - token_metadata: s.token_metadata, - operators: s.operators, - token_ids: s.token_ids - } - ); - return [ - ret2[0], - { - ...s, - ledger: ret2[1].ledger, - metadata: ret2[1].metadata, - token_metadata: ret2[1].token_metadata, - operators: ret2[1].operators, - token_ids: ret2[1].token_ids - } - ] - }, - Balance_of: (p: NFT.balance_of) => { - const ret2: [list, NFT.storage] = - NFT.balance_of( - p, - { - ledger: s.ledger, - metadata: s.metadata, - token_metadata: s.token_metadata, - operators: s.operators, - token_ids: s.token_ids - } - ); - return [ - ret2[0], - { - ...s, - ledger: ret2[1].ledger, - metadata: ret2[1].metadata, - token_metadata: ret2[1].token_metadata, - operators: ret2[1].operators, - token_ids: ret2[1].token_ids - } - ] - }, - Update_operators: (p: NFT.update_operators) => { - const ret2: [list, NFT.storage] = - NFT.update_ops( - p, - { - ledger: s.ledger, - metadata: s.metadata, - token_metadata: s.token_metadata, - operators: s.operators, - token_ids: s.token_ids - } - ); - return [ - ret2[0], - { - ...s, - ledger: ret2[1].ledger, - metadata: ret2[1].metadata, - token_metadata: ret2[1].token_metadata, - operators: ret2[1].operators, - token_ids: ret2[1].token_ids - } - ] - } - } - ); +export type storage = { + administrators: set
, + offers: map, //user sells an offer + ledger: FA2Impl.NFT.ledger, + metadata: FA2Impl.TZIP16.metadata, + token_metadata: FA2Impl.TZIP12.tokenMetadata, + operators: FA2Impl.NFT.operators +}; ``` Explanation: - an `offer` is an NFT _(owned by someone)_ with a price - `storage` has a new field to store `offers`: a `map` of offers -- `parameter` has two new entrypoints `buy` and `sell` -- `main` function exposes these two new entrypoints -Update also the initial storage on file `nft.storages.jsligo` to initialize `offers` +Update the initial storage on file `nft.storageList.jsligo` to initialize `offers` field. Here is what it should look like : ```ligolang -#include "nft.jsligo" -const default_storage = - {administrators: Set.literal(list(["tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb" - as address])) - as set
, - offers: Map.empty as map, - ledger: Big_map.empty as NFT.Ledger.t, - metadata: Big_map.empty as NFT.Metadata.t, - token_metadata: Big_map.empty as NFT.TokenMetadata.t, - operators: Big_map.empty as NFT.Operators.t, - token_ids: Set.empty as set - }; +#import "nft.jsligo" "Contract" + +const default_storage : Contract.storage = { + administrators: Set.literal( + list(["tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb" as address]) + ) as set
, + offers: Map.empty as map, + ledger: Big_map.empty as Contract.FA2Impl.NFT.ledger, + metadata: Big_map.literal( + list( + [ + ["", bytes `tezos-storage:data`], + [ + "data", + bytes + `{ + "name":"FA2 NFT Marketplace", + "description":"Example of FA2 implementation", + "version":"0.0.1", + "license":{"name":"MIT"}, + "authors":["Marigold"], + "homepage":"https://marigold.dev", + "source":{ + "tools":["Ligo"], + "location":"https://github.com/ligolang/contract-catalogue/tree/main/lib/fa2"}, + "interfaces":["TZIP-012"], + "errors": [], + "views": [] + }` + ] + ] + ) + ) as Contract.FA2Impl.TZIP16.metadata, + token_metadata: Big_map.empty as Contract.FA2Impl.TZIP12.tokenMetadata, + operators: Big_map.empty as Contract.FA2Impl.NFT.operators, +}; ``` Finally, compile the contract ```bash -TAQ_LIGO_IMAGE=ligolang/ligo:0.64.2 taq compile nft.jsligo +TAQ_LIGO_IMAGE=ligolang/ligo:1.0.0 taq compile nft.jsligo ``` ### Sell at an offer price @@ -178,249 +104,102 @@ TAQ_LIGO_IMAGE=ligolang/ligo:0.64.2 taq compile nft.jsligo Define the `sell` function as below: ```ligolang -const sell = (token_id : nat,price : nat, s : storage) : ret => { - +@entry +const sell = ([token_id, price]: [nat, nat], s: storage): ret => { //check balance of seller - const sellerBalance = NFT.Storage.get_balance({ledger:s.ledger,metadata:s.metadata,operators:s.operators,token_metadata:s.token_metadata,token_ids : s.token_ids},Tezos.get_source(),token_id); - if(sellerBalance != (1 as nat)) return failwith("2"); + const sellerBalance = + FA2Impl.NFT.get_balance( + [Tezos.get_source(), token_id], + { + ledger: s.ledger, + metadata: s.metadata, + operators: s.operators, + token_metadata: s.token_metadata, + } + ); + if (sellerBalance != (1 as nat)) return failwith("2"); //need to allow the contract itself to be an operator on behalf of the seller - const newOperators = NFT.Operators.add_operator(s.operators,Tezos.get_source(),Tezos.get_self_address(),token_id); + const newOperators = + FA2Impl.Sidecar.add_operator( + s.operators, + Tezos.get_source(), + Tezos.get_self_address(), + token_id + ); //DECISION CHOICE: if offer already exists, we just override it - return [list([]) as list,{...s,offers:Map.add(token_id,{owner : Tezos.get_source(), price : price},s.offers),operators:newOperators}]; -}; -``` - -Then call it in the `main` function to do the right business operations -```ligolang -const main = ([p, s]: [parameter, storage]): ret => - match( - p, + return [ + list([]) as list, { - Mint: (p: [nat, bytes, bytes, bytes, bytes]) => - mint(p[0], p[1], p[2], p[3], p[4], s), - Buy: (p: [nat, address]) => [list([]), s], - Sell: (p : [nat,nat]) => sell(p[0],p[1], s), - AddAdministrator: (p: address) => { - if (Set.mem(Tezos.get_sender(), s.administrators)) { - return [ - list([]), - { ...s, administrators: Set.add(p, s.administrators) } - ] - } else { - return failwith("1") - } - }, - Transfer: (p: NFT.transfer) => { - const ret2: [list, NFT.storage] = - NFT.transfer( - p, - { - ledger: s.ledger, - metadata: s.metadata, - token_metadata: s.token_metadata, - operators: s.operators, - token_ids: s.token_ids - } - ); - return [ - ret2[0], - { - ...s, - ledger: ret2[1].ledger, - metadata: ret2[1].metadata, - token_metadata: ret2[1].token_metadata, - operators: ret2[1].operators, - token_ids: ret2[1].token_ids - } - ] - }, - Balance_of: (p: NFT.balance_of) => { - const ret2: [list, NFT.storage] = - NFT.balance_of( - p, - { - ledger: s.ledger, - metadata: s.metadata, - token_metadata: s.token_metadata, - operators: s.operators, - token_ids: s.token_ids - } - ); - return [ - ret2[0], - { - ...s, - ledger: ret2[1].ledger, - metadata: ret2[1].metadata, - token_metadata: ret2[1].token_metadata, - operators: ret2[1].operators, - token_ids: ret2[1].token_ids - } - ] - }, - Update_operators: (p: NFT.update_operators) => { - const ret2: [list, NFT.storage] = - NFT.update_ops( - p, - { - ledger: s.ledger, - metadata: s.metadata, - token_metadata: s.token_metadata, - operators: s.operators, - token_ids: s.token_ids - } - ); - return [ - ret2[0], - { - ...s, - ledger: ret2[1].ledger, - metadata: ret2[1].metadata, - token_metadata: ret2[1].token_metadata, - operators: ret2[1].operators, - token_ids: ret2[1].token_ids - } - ] - } + ...s, + offers: Map.add( + token_id, + { owner: Tezos.get_source(), price: price }, + s.offers + ), + operators: newOperators } - ); + ] +}; ``` Explanation: - User must have enough tokens _(wine bottles)_ to place an offer - the seller will set the NFT marketplace smart contract as an operator. When the buyer sends his money to buy the NFT, the smart contract will change the NFT ownership _(it is not interactive with the seller, the martketplace will do it on behalf of the seller based on the offer data)_ -- we update the `storage` to publish the offer -- finally, do the correct business by calling `sell` function inside the `sell` case on `main` +- `storage` is updated with `offer` field ### Buy a bottle on the marketplace -Now that we have offers available on the marketplace, let's buy bottles! +Now that there are offers available on the marketplace, let's buy bottles! Edit the smart contract to add the `buy` feature ```ligolang -const buy = (token_id : nat, seller : address, s : storage) : ret => { - +@entry +const buy = ([token_id, seller]: [nat, address], s: storage): ret => { //search for the offer - return match( Map.find_opt(token_id,s.offers) , { - None : () => failwith("3"), - Some : (offer : offer) => { - - //check if amount have been paid enough - if(Tezos.get_amount() < offer.price * (1 as mutez)) return failwith("5"); - - // prepare transfer of XTZ to seller - const op = Tezos.transaction(unit,offer.price * (1 as mutez),Tezos.get_contract_with_error(seller,"6")); - //transfer tokens from seller to buyer - const ledger = NFT.Ledger.transfer_token_from_user_to_user(s.ledger,token_id,seller,Tezos.get_source()); - - //remove offer - return [list([op]) as list, {...s, offers : Map.update(token_id,None(),s.offers), ledger : ledger}]; - } - }); -}; -``` - -Call `buy` function on `main` - -```ligolang -const main = ([p, s]: [parameter, storage]): ret => - match( - p, - { - Mint: (p: [nat, bytes, bytes, bytes, bytes]) => - mint(p[0], p[1], p[2], p[3], p[4], s), - Buy: (p: [nat, address]) => buy(p[0], p[1], s), - Sell: (p: [nat, nat]) => sell(p[0], p[1], s), - AddAdministrator: (p: address) => { - if (Set.mem(Tezos.get_sender(), s.administrators)) { - return [ - list([]), - { ...s, administrators: Set.add(p, s.administrators) } - ] - } else { - return failwith("1") - } - }, - Transfer: (p: NFT.transfer) => { - const ret2: [list, NFT.storage] = - NFT.transfer( - p, - { - ledger: s.ledger, - metadata: s.metadata, - token_metadata: s.token_metadata, - operators: s.operators, - token_ids: s.token_ids - } - ); - return [ - ret2[0], - { - ...s, - ledger: ret2[1].ledger, - metadata: ret2[1].metadata, - token_metadata: ret2[1].token_metadata, - operators: ret2[1].operators, - token_ids: ret2[1].token_ids - } - ] - }, - Balance_of: (p: NFT.balance_of) => { - const ret2: [list, NFT.storage] = - NFT.balance_of( - p, - { - ledger: s.ledger, - metadata: s.metadata, - token_metadata: s.token_metadata, - operators: s.operators, - token_ids: s.token_ids - } + return match(Map.find_opt(token_id, s.offers)) { + when (None()): + failwith("3") + when (Some(offer)): + do { + //check if amount have been paid enough + + if (Tezos.get_amount() < offer.price * (1 as mutez)) return failwith( + "5" + ); + // prepare transfer of XTZ to seller + + const op = + Tezos.transaction( + unit, + offer.price * (1 as mutez), + Tezos.get_contract_with_error(seller, "6") ); - return [ - ret2[0], - { - ...s, - ledger: ret2[1].ledger, - metadata: ret2[1].metadata, - token_metadata: ret2[1].token_metadata, - operators: ret2[1].operators, - token_ids: ret2[1].token_ids - } - ] - }, - Update_operators: (p: NFT.update_operators) => { - const ret2: [list, NFT.storage] = - NFT.update_ops( - p, - { - ledger: s.ledger, - metadata: s.metadata, - token_metadata: s.token_metadata, - operators: s.operators, - token_ids: s.token_ids - } + //transfer tokens from seller to buyer + + const ledger = + FA2Impl.Sidecar.transfer_token_from_user_to_user( + s.ledger, + token_id, + seller, + Tezos.get_source() ); + //remove offer + return [ - ret2[0], + list([op]) as list, { - ...s, - ledger: ret2[1].ledger, - metadata: ret2[1].metadata, - token_metadata: ret2[1].token_metadata, - operators: ret2[1].operators, - token_ids: ret2[1].token_ids + ...s, offers: Map.update(token_id, None(), s.offers), ledger: ledger } ] } - } - ); + } +}; ``` Explanation: @@ -428,14 +207,13 @@ Explanation: - search for the offer based on the `token_id` or return an error if it does not exist - check that the amount sent by the buyer is greater than the offer price. If it is ok, transfer the offer price to the seller and transfer the NFT to the buyer - remove the offer as it has been executed -- finally, do the correct business by calling `sell` function inside the `sell` case on `main` ### Compile and deploy -We finished the smart contract implementation of this second training, let's deploy to ghostnet. +Smart contract implementation of this second training is finished, let's deploy to ghostnet. ```bash -TAQ_LIGO_IMAGE=ligolang/ligo:0.64.2 taq compile nft.jsligo +TAQ_LIGO_IMAGE=ligolang/ligo:1.0.0 taq compile nft.jsligo taq deploy nft.tz -e "testing" ``` @@ -443,11 +221,11 @@ taq deploy nft.tz -e "testing" β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Contract β”‚ Address β”‚ Alias β”‚ Balance In Mutez β”‚ Destination β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ nft.tz β”‚ KT1J9QpWT8awyYiFJSpEWqZtVYWKVrbm1idY β”‚ nft β”‚ 0 β”‚ https://ghostnet.ecadinfra.com β”‚ +β”‚ nft.tz β”‚ KT1KyV1Hprert33AAz5B94CLkqAHdKZU56dq β”‚ nft β”‚ 0 β”‚ https://ghostnet.ecadinfra.com β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` -** We have implemented and deployed the smart contract (backend)!** +**Smart contract (backend) is implmented and deployed!** ## NFT Marketplace front @@ -457,7 +235,7 @@ Generate Typescript classes and go to the frontend to run the server taq generate types ./app/src cd ./app yarn install -yarn run start +yarn dev ``` ## Sale page @@ -470,6 +248,8 @@ Add this code inside the file : import { InfoOutlined } from "@mui/icons-material"; import SellIcon from "@mui/icons-material/Sell"; +import * as api from "@tzkt/sdk-api"; + import { Box, Button, @@ -512,13 +292,17 @@ type Offer = { }; export default function OffersPage() { + api.defaults.baseUrl = "https://api.ghostnet.tzkt.io"; + const [selectedTokenId, setSelectedTokenId] = React.useState(0); const [currentPageIndex, setCurrentPageIndex] = useState(1); - let [offersTokenIDMap, setOffersTokenIDMap] = React.useState>( - new Map() + let [offersTokenIDMap, setOffersTokenIDMap] = React.useState< + Map + >(new Map()); + let [ownerTokenIds, setOwnerTokenIds] = React.useState>( + new Set() ); - let [ownerTokenIds, setOwnerTokenIds] = React.useState>(new Set()); const { nftContrat, @@ -551,20 +335,31 @@ export default function OffersPage() { ownerTokenIds = new Set(); offersTokenIDMap = new Map(); + const token_metadataBigMapId = ( + storage.token_metadata as unknown as { id: BigNumber } + ).id.toNumber(); + + const token_ids = await api.bigMapsGetKeys(token_metadataBigMapId, { + micheline: "Json", + active: true, + }); + await Promise.all( - storage.token_ids.map(async (token_id) => { - let owner = await storage.ledger.get(token_id); + token_ids.map(async (token_idKey) => { + const token_idNat = new BigNumber(token_idKey.key) as nat; + + let owner = await storage.ledger.get(token_idNat); if (owner === userAddress) { - ownerTokenIds.add(token_id); + ownerTokenIds.add(token_idKey.key); - const ownerOffers = await storage.offers.get(token_id); - if (ownerOffers) offersTokenIDMap.set(token_id, ownerOffers); + const ownerOffers = await storage.offers.get(token_idNat); + if (ownerOffers) offersTokenIDMap.set(token_idKey.key, ownerOffers); console.log( "found for " + owner + " on token_id " + - token_id + + token_idKey.key + " with balance " + 1 ); @@ -671,9 +466,8 @@ export default function OffersPage() { {"Description : " + - nftContratTokenMetadataMap.get( - token_id.toNumber() - )?.description} + nftContratTokenMetadataMap.get(token_id) + ?.description} } @@ -681,16 +475,14 @@ export default function OffersPage() { } - title={ - nftContratTokenMetadataMap.get(token_id.toNumber())?.name - } + title={nftContratTokenMetadataMap.get(token_id)?.name} /> { - setSelectedTokenId(token_id.toNumber()); + setSelectedTokenId(Number(token_id)); formik.handleSubmit(values); }} > @@ -781,33 +573,25 @@ export default function OffersPage() { } ``` -Explanation: +Explanation : -- the template will display all owned NFTs. Only NFTs belonging to the logged user are selected -- for each NFT, we have a form to make an offer at a price -- if you do an offer, it calls the `sell` function and the smart contract entrypoint `nftContrat?.methods.sell(BigNumber(token_id) as nat,BigNumber(price * 1000000) as nat).send()`. We multiply the XTZ price by 10^6 because the smart contract manipulates mutez. +- the template displays all owned NFTs. Only NFTs belonging to the logged user are selected +- for each NFT, there is a form to make an offer at a price +- if you do an offer, it calls the `sell` function and the smart contract entrypoint `nftContrat?.methods.sell(BigNumber(token_id) as nat,BigNumber(price * 1000000) as nat).send()`. Multiply the XTZ price by 10^6 because the smart contract manipulates mutez. ## Let's play : Sell -1. Connect with your wallet and choose `alice` account (or one of the administrators you set on the smart contract earlier). You are redirected to the Administration /mint page as there is no NFT minted yet - -2. Enter these values on the form for example : - -- `name`: Saint Emilion - Franc la Rose -- `symbol`: SEMIL -- `description`: Grand cru 2007 - -3. Click on `Upload an image` and select a bottle picture on your computer - -4. Click on the Mint button - -Your picture will be pushed to IPFS and displayed, then your wallet ask you to sign the mint operation. - -- Confirm operation +- Connect with your wallet and choose **alice** account (or one of the administrators you set on the smart contract earlier). You are redirected to the Administration /mint page as there is no NFT minted yet +- Enter these values on the form for example : + - `name`: Saint Emilion - Franc la Rose + - `symbol`: SEMIL + - `description`: Grand cru 2007 +- Click on **Upload an image** and select a bottle picture on your computer +- Click on the Mint button -- Wait less than 1 minute until you get the confirmation notification, the page will automatically be refreshed. +Your picture is pushed to IPFS and displayed, then your wallet ask you to sign the mint operation. -5. Now, go to the `Trading` menu and the `Sell bottles` submenu. +5. Now, go to the **Trading** menu and the **Sell bottles** submenu. 6. Click on the submenu entry @@ -816,7 +600,7 @@ Your picture will be pushed to IPFS and displayed, then your wallet ask you to s You are the owner of this bottle so you can create an offer to sell it. - Enter a price offer -- Click on `SELL` button +- Click on **SELL** button - Wait a bit for the confirmation, then after auto-refresh you have an offer for this NFT ## Wine Catalogue page @@ -973,7 +757,7 @@ export default function WineCataloguePage() { {"Description : " + nftContratTokenMetadataMap.get( - token_id.toNumber() + token_id.toString() )?.description} @@ -986,7 +770,7 @@ export default function WineCataloguePage() { } title={ - nftContratTokenMetadataMap.get(token_id.toNumber())?.name + nftContratTokenMetadataMap.get(token_id.toString())?.name } /> , - totalSupply: nat, - offers: map, //user sells an offer - ledger: SINGLEASSET.Ledger.t, - metadata: SINGLEASSET.Metadata.t, - token_metadata: SINGLEASSET.TokenMetadata.t, - operators: SINGLEASSET.Operators.t, - owners: set - }; +export type storage = { + administrators: set
, + totalSupply: nat, + offers: map, //user sells an offer + + ledger: FA2Impl.Datatypes.ledger, + metadata: FA2Impl.TZIP16.metadata, + token_metadata: FA2Impl.TZIP12.tokenMetadata, + operators: FA2Impl.Datatypes.operators, +}; ``` Explanation: - `offers` is now a `map`, because you don't have to store `token_id` as a key, now the key is the owner's address. Each owner can sell a part of the unique collection -- `offer` requires a quantity, each owner will sell a part of the unique collection +- `offer` requires a quantity, each owner is selling a part of the unique collection - `totalSupply` is set while minting in order to track the global quantity of minted items on the collection. It makes it unnecessary to recalculate each time the quantity from each owner's holdings (this value is constant) -- Because the ledger is made of `big_map` of key `owners`, we cache the keys to be able to loop on it -- Since we have a unique collection, we remove `token_ids`. `token_id` will be set to `0` - -We don't change the `parameter` type because the signature is the same, but you can edit the comments because it is not the same parameter anymore and also changes to the new namespace `SINGLEASSET` - -```ligolang -type parameter = - | ["Mint", nat,bytes,bytes,bytes,bytes] // quantity, name , description ,symbol , bytesipfsUrl - | ["Buy", nat, address] //buy quantity at a seller offer price - | ["Sell", nat, nat] //sell quantity at a price - | ["AddAdministrator" , address] - | ["Transfer", SINGLEASSET.transfer] - | ["Balance_of", SINGLEASSET.balance_of] - | ["Update_operators", SINGLEASSET.update_operators]; -``` Edit the `mint` function to add the `quantity` extra param, and finally change the `return` ```ligolang -const mint = (quantity : nat, name : bytes, description : bytes ,symbol : bytes , ipfsUrl : bytes, s : storage) : ret => { - - if(quantity <= (0 as nat)) return failwith("0"); - - if(! Set.mem(Tezos.get_sender(), s.administrators)) return failwith("1"); - - const token_info: map = - Map.literal(list([ - ["name", name], - ["description",description], - ["interfaces", (bytes `["TZIP-12"]`)], - ["thumbnailUri", ipfsUrl], - ["symbol",symbol], - ["decimals", (bytes `0`)] - ])) as map; - - - const metadata : bytes = bytes - `{ - "name":"FA2 NFT Marketplace", - "description":"Example of FA2 implementation", - "version":"0.0.1", - "license":{"name":"MIT"}, - "authors":["Marigold"], - "homepage":"https://marigold.dev", - "source":{ - "tools":["Ligo"], - "location":"https://github.com/ligolang/contract-catalogue/tree/main/lib/fa2"}, - "interfaces":["TZIP-012"], - "errors": [], - "views": [] - }` ; - - return [list([]) as list, - {...s, - totalSupply: quantity, - ledger: Big_map.literal(list([[Tezos.get_sender(),quantity as nat]])) as SINGLEASSET.Ledger.t, - metadata : Big_map.literal(list([["", bytes `tezos-storage:data`],["data", metadata]])), - token_metadata: Big_map.add(0 as nat, {token_id: 0 as nat,token_info:token_info},s.token_metadata), - operators: Big_map.empty as SINGLEASSET.Operators.t, - owners: Set.add(Tezos.get_sender(),s.owners)}]; - }; +@entry +const mint = ( + [quantity, name, description, symbol, ipfsUrl]: [ + nat, + bytes, + bytes, + bytes, + bytes + ], + s: storage +): ret => { + if (quantity <= (0 as nat)) return failwith("0"); + if (! Set.mem(Tezos.get_sender(), s.administrators)) return failwith("1"); + const token_info: map = + Map.literal( + list( + [ + ["name", name], + ["description", description], + ["interfaces", (bytes `["TZIP-12"]`)], + ["artifactUri", ipfsUrl], + ["displayUri", ipfsUrl], + ["thumbnailUri", ipfsUrl], + ["symbol", symbol], + ["decimals", (bytes `0`)] + ] + ) + ) as map; + return [ + list([]) as list, + { + ...s, + totalSupply: quantity, + ledger: Big_map.literal(list([[Tezos.get_sender(), quantity as nat]])) as + FA2Impl.SingleAsset.ledger, + token_metadata: Big_map.add( + 0 as nat, + { token_id: 0 as nat, token_info: token_info }, + s.token_metadata + ), + operators: Big_map.empty as FA2Impl.SingleAsset.operators, + } + ] +}; ``` -Edit the `sell` function to replace `token_id` by `quantity`, we add/override an offer for the user +Edit the `sell` function to replace `token_id` by `quantity`, add/override an offer for the user ```ligolang -const sell = (quantity: nat, price: nat, s: storage) : ret => { - +@entry +const sell = ([quantity, price]: [nat, nat], s: storage): ret => { //check balance of seller - const sellerBalance = SINGLEASSET.Storage.get_amount_for_owner({ledger:s.ledger,metadata:s.metadata,operators:s.operators,token_metadata:s.token_metadata,owners:s.owners})(Tezos.get_source()); - if(quantity > sellerBalance) return failwith("2"); + const sellerBalance = + FA2Impl.Sidecar.get_amount_for_owner( + { + ledger: s.ledger, + metadata: s.metadata, + operators: s.operators, + token_metadata: s.token_metadata, + } + )(Tezos.get_source()); + if (quantity > sellerBalance) return failwith("2"); //need to allow the contract itself to be an operator on behalf of the seller - const newOperators = SINGLEASSET.Operators.add_operator(s.operators)(Tezos.get_source())(Tezos.get_self_address()); + const newOperators = + FA2Impl.Sidecar.add_operator(s.operators)(Tezos.get_source())( + Tezos.get_self_address() + ); //DECISION CHOICE: if offer already exists, we just override it - return [list([]) as list,{...s,offers:Map.add(Tezos.get_source(),{quantity : quantity, price : price},s.offers),operators:newOperators}]; + + return [ + list([]) as list, + { + ...s, + offers: Map.add( + Tezos.get_source(), + { quantity: quantity, price: price }, + s.offers + ), + operators: newOperators + } + ] }; ``` Also edit the `buy` function to replace `token_id` by `quantity`, check quantities, check final price is enough and update the current offer ```ligolang -const buy = (quantity: nat, seller: address, s: storage) : ret => { - +@entry +const buy = ([quantity, seller]: [nat, address], s: storage): ret => { //search for the offer - return match( Map.find_opt(seller,s.offers) , { - None : () => failwith("3"), - Some : (offer : offer) => { - //check if quantity is enough - if(quantity > offer.quantity) return failwith("4"); - //check if amount have been paid enough - if(Tezos.get_amount() < (offer.price * quantity) * (1 as mutez)) return failwith("5"); - // prepare transfer of XTZ to seller - const op = Tezos.transaction(unit,(offer.price * quantity) * (1 as mutez),Tezos.get_contract_with_error(seller,"6")); + return match(Map.find_opt(seller, s.offers)) { + when (None()): + failwith("3") + when (Some(offer)): + do { + //check if quantity is enough - //transfer tokens from seller to buyer - let ledger = SINGLEASSET.Ledger.decrease_token_amount_for_user(s.ledger)(seller)(quantity); - ledger = SINGLEASSET.Ledger.increase_token_amount_for_user(ledger)(Tezos.get_source())(quantity); + if (quantity > offer.quantity) return failwith("4"); + //check if amount have been paid enough - //update new offer - const newOffer = {...offer,quantity : abs(offer.quantity - quantity)}; - - return [list([op]) as list, {...s, offers : Map.update(seller,Some(newOffer),s.offers), ledger : ledger, owners : Set.add(Tezos.get_source(),s.owners)}]; - } - }); -}; -``` - -Finally, update the namespaces and replace `token_ids` by owners on the `main` function - -```ligolang -const main = ([p, s]: [parameter,storage]): ret => - match(p, { - Mint: (p: [nat,bytes,bytes,bytes,bytes]) => mint(p[0],p[1],p[2],p[3],p[4],s), - Buy: (p : [nat,address]) => buy(p[0],p[1],s), - Sell: (p : [nat,nat]) => sell(p[0],p[1], s), - AddAdministrator : (p : address) => {if(Set.mem(Tezos.get_sender(), s.administrators)){ return [list([]),{...s,administrators:Set.add(p, s.administrators)}]} else {return failwith("1");}} , - Transfer: (p: SINGLEASSET.transfer) => { - const ret2 : [list, SINGLEASSET.storage] = SINGLEASSET.transfer(p)({ledger:s.ledger,metadata:s.metadata,token_metadata:s.token_metadata,operators:s.operators,owners:s.owners}); - return [ret2[0],{...s,ledger:ret2[1].ledger,metadata:ret2[1].metadata,token_metadata:ret2[1].token_metadata,operators:ret2[1].operators,owners:ret2[1].owners}]; - }, - Balance_of: (p: SINGLEASSET.balance_of) => { - const ret2 : [list, SINGLEASSET.storage] = SINGLEASSET.balance_of(p)({ledger:s.ledger,metadata:s.metadata,token_metadata:s.token_metadata,operators:s.operators,owners:s.owners}); - return [ret2[0],{...s,ledger:ret2[1].ledger,metadata:ret2[1].metadata,token_metadata:ret2[1].token_metadata,operators:ret2[1].operators,owners:ret2[1].owners}]; - }, - Update_operators: (p: SINGLEASSET.update_operators) => { - const ret2 : [list, SINGLEASSET.storage] = SINGLEASSET.update_ops(p)({ledger:s.ledger,metadata:s.metadata,token_metadata:s.token_metadata,operators:s.operators,owners:s.owners}); - return [ret2[0],{...s,ledger:ret2[1].ledger,metadata:ret2[1].metadata,token_metadata:ret2[1].token_metadata,operators:ret2[1].operators,owners:ret2[1].owners}]; + if (Tezos.get_amount() < (offer.price * quantity) * (1 as mutez)) return failwith( + "5" + ); + // prepare transfer of XTZ to seller + + const op = + Tezos.transaction( + unit, + (offer.price * quantity) * (1 as mutez), + Tezos.get_contract_with_error(seller, "6") + ); + //transfer tokens from seller to buyer + + let ledger = + FA2Impl.Sidecar.decrease_token_amount_for_user(s.ledger)(seller)( + quantity + ); + ledger + = FA2Impl.Sidecar.increase_token_amount_for_user(ledger)( + Tezos.get_source() + )(quantity); + //update new offer + + const newOffer = { ...offer, quantity: abs(offer.quantity - quantity) }; + return [ + list([op]) as list, + { + ...s, + offers: Map.update(seller, Some(newOffer), s.offers), + ledger: ledger, + } + ] } - }); + } +}; ``` Edit the storage file `nft.storageList.jsligo` as it. (:warning: you can change the `administrator` address to your own address or keep `alice`) ```ligolang -#include "nft.jsligo" -const default_storage = -{ - administrators: Set.literal(list(["tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb" as address])) as set
, +#import "nft.jsligo" "Contract" + +const default_storage: Contract.storage = { + administrators: Set.literal( + list(["tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb" as address]) + ) as set
, totalSupply: 0 as nat, - offers: Map.empty as map, - ledger: Big_map.empty as SINGLEASSET.Ledger.t, - metadata: Big_map.empty as SINGLEASSET.Metadata.t, - token_metadata: Big_map.empty as SINGLEASSET.TokenMetadata.t, - operators: Big_map.empty as SINGLEASSET.Operators.t, - owners: Set.empty as set, - } -; + offers: Map.empty as map, + ledger: Big_map.empty as Contract.FA2Impl.SingleAsset.ledger, + metadata: Big_map.literal( + list( + [ + ["", bytes `tezos-storage:data`], + [ + "data", + bytes + `{ + "name":"FA2 NFT Marketplace", + "description":"Example of FA2 implementation", + "version":"0.0.1", + "license":{"name":"MIT"}, + "authors":["Marigold"], + "homepage":"https://marigold.dev", + "source":{ + "tools":["Ligo"], + "location":"https://github.com/ligolang/contract-catalogue/tree/main/lib/fa2"}, + "interfaces":["TZIP-012"], + "errors": [], + "views": [] + }` + ] + ] + ) + ) as Contract.FA2Impl.TZIP16.metadata, + token_metadata: Big_map.empty as Contract.FA2Impl.TZIP12.tokenMetadata, + operators: Big_map.empty as Contract.FA2Impl.SingleAsset.operators, +}; ``` Compile again and deploy to ghostnet. ```bash -TAQ_LIGO_IMAGE=ligolang/ligo:0.64.2 taq compile nft.jsligo +TAQ_LIGO_IMAGE=ligolang/ligo:1.0.0 taq compile nft.jsligo taq deploy nft.tz -e "testing" ``` @@ -221,13 +255,13 @@ taq deploy nft.tz -e "testing" β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Contract β”‚ Address β”‚ Alias β”‚ Balance In Mutez β”‚ Destination β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ nft.tz β”‚ KT1SYqk9tAhgExhLawfvwc3ZCfGNzYjwi38n β”‚ nft β”‚ 0 β”‚ https://ghostnet.ecadinfra.com β”‚ +β”‚ nft.tz β”‚ KT1EUWEeR9RHMb5q5jeW5jbhxBFHbLTqQgiZ β”‚ nft β”‚ 0 β”‚ https://ghostnet.ecadinfra.com β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` -We finished the smart contract! _(backend)_ +**The smart contract! _(backend)_ is finished** -# NFT Marketplace front +## NFT Marketplace front Generate Typescript classes and go to the frontend to run the server @@ -235,12 +269,12 @@ Generate Typescript classes and go to the frontend to run the server taq generate types ./app/src cd ./app yarn install -yarn run start +yarn dev ``` -## Update in `App.tsx` +### Update in `App.tsx` -We just need to fetch the token_id == 0. +Fetch the `token_id == 0`. Replace the function `refreshUserContextOnPageReload` by ```typescript @@ -260,7 +294,7 @@ const refreshUserContextOnPageReload = async () => { let tokenMetadata: TZIP21TokenMetadata = (await c .tzip12() .getTokenMetadata(0)) as TZIP21TokenMetadata; - nftContratTokenMetadataMap.set(0, tokenMetadata); + nftContratTokenMetadataMap.set("0", tokenMetadata); setNftContratTokenMetadataMap(new Map(nftContratTokenMetadataMap)); //new Map to force refresh } catch (error) { @@ -287,7 +321,7 @@ const refreshUserContextOnPageReload = async () => { ### Update in `MintPage.tsx` -We introduce the quantity and remove the `token_id` variable. Replace the full file with the following content: +The quantity field is added and the `token_id` field is removed. Replace the full file by the following content: ```typescript import OpenWithIcon from "@mui/icons-material/OpenWith"; @@ -392,11 +426,11 @@ export default function MintPage() { const requestHeaders: HeadersInit = new Headers(); requestHeaders.set( "pinata_api_key", - `${process.env.REACT_APP_PINATA_API_KEY}` + `${import.meta.env.VITE_PINATA_API_KEY}` ); requestHeaders.set( "pinata_secret_api_key", - `${process.env.REACT_APP_PINATA_API_SECRET}` + `${import.meta.env.VITE_PINATA_API_SECRET}` ); const resFile = await fetch( @@ -721,11 +755,12 @@ export default function MintPage() { ### Update in `OffersPage.tsx` -We introduce the quantity and remove the `token_id` variable. Replace the full file with the following content: +The quantity field is added and the `token_id` filed is removed. Replace the full file with the following content: ```typescript import { InfoOutlined } from "@mui/icons-material"; import SellIcon from "@mui/icons-material/Sell"; +import * as api from "@tzkt/sdk-api"; import { Box, @@ -773,6 +808,8 @@ type Offer = { }; export default function OffersPage() { + api.defaults.baseUrl = "https://api.ghostnet.tzkt.io"; + const [selectedTokenId, setSelectedTokenId] = React.useState(0); const [currentPageIndex, setCurrentPageIndex] = useState(1); @@ -809,9 +846,18 @@ export default function OffersPage() { if (storage) { console.log("context is not empty, init page now"); + const ledgerBigMapId = ( + storage.ledger as unknown as { id: BigNumber } + ).id.toNumber(); + + const ownersKeys = await api.bigMapsGetKeys(ledgerBigMapId, { + micheline: "Json", + active: true, + }); + await Promise.all( - storage.owners.map(async (owner) => { - if (owner === userAddress) { + ownersKeys.map(async (ownerKey) => { + if (ownerKey.key === userAddress) { const ownerBalance = await storage.ledger.get( userAddress as address ); @@ -824,7 +870,7 @@ export default function OffersPage() { console.log( "found for " + - owner + + ownerKey.key + " on token_id " + 0 + " with balance " + @@ -917,7 +963,7 @@ export default function OffersPage() { {"ID : " + 0} {"Description : " + - nftContratTokenMetadataMap.get(0)?.description} + nftContratTokenMetadataMap.get("0")?.description} } @@ -925,14 +971,14 @@ export default function OffersPage() { } - title={nftContratTokenMetadataMap.get(0)?.name} + title={nftContratTokenMetadataMap.get("0")?.name} /> setCurrentPageIndex(value)} count={Math.ceil( - Array.from(storage?.offers.entries()).filter(([key, offer]) => + Array.from(storage?.offers.entries()).filter(([_, offer]) => offer.quantity.isGreaterThan(0) ).length / itemPerPage )} @@ -1181,7 +1227,7 @@ export default function WineCataloguePage() { > {Array.from(storage?.offers.entries()) .filter(([_, offer]) => offer.quantity.isGreaterThan(0)) - .filter((owner, index) => + .filter((_, index) => index >= currentPageIndex * itemPerPage - itemPerPage && index < currentPageIndex * itemPerPage ? true @@ -1197,7 +1243,8 @@ export default function WineCataloguePage() { {"ID : " + 0} {"Description : " + - nftContratTokenMetadataMap.get(0)?.description} + nftContratTokenMetadataMap.get("0") + ?.description} {"Seller : " + owner} @@ -1206,14 +1253,14 @@ export default function WineCataloguePage() { } - title={nftContratTokenMetadataMap.get(0)?.name} + title={nftContratTokenMetadataMap.get("0")?.name} /> , - offers: map<[address,nat],offer>, //user sells an offer for a token_id - ledger: MULTIASSET.Ledger.t, - metadata: MULTIASSET.Metadata.t, - token_metadata: MULTIASSET.TokenMetadata.t, - operators: MULTIASSET.Operators.t, - owner_token_ids : set<[MULTIASSET.Storage.owner,MULTIASSET.Storage.token_id]>, - token_ids : set - }; -``` +export type storage = { + administrators: set
, + offers: map<[address, nat], offer>, //user sells an offer for a token_id -Update `parameter` type too - -```ligolang -type parameter = - | ["Mint", nat,nat,bytes,bytes,bytes,bytes] //token_id, quantity, name , description ,symbol , bytesipfsUrl - | ["AddAdministrator" , address] - | ["Buy", nat,nat, address] //buy token_id,quantity at a seller offer price - | ["Sell", nat,nat, nat] //sell token_id,quantity at a price - | ["Transfer", MULTIASSET.transfer] - | ["Balance_of", MULTIASSET.balance_of] - | ["Update_operators", MULTIASSET.update_operators]; + ledger: FA2Impl.Datatypes.ledger, + metadata: FA2Impl.TZIP16.metadata, + token_metadata: FA2Impl.TZIP12.tokenMetadata, + operators: FA2Impl.Datatypes.operators, +}; ``` Update `mint` function ```ligolang -const mint = (token_id : nat, quantity: nat, name : bytes, description : bytes,symbol : bytes, ipfsUrl: bytes, s: storage) : ret => { - - if(quantity <= (0 as nat)) return failwith("0"); - - if(! Set.mem(Tezos.get_sender(), s.administrators)) return failwith("1"); - - const token_info: map = - Map.literal(list([ - ["name", name], - ["description",description], - ["interfaces", (bytes `["TZIP-12"]`)], - ["thumbnailUri", ipfsUrl], - ["symbol",symbol], - ["decimals", (bytes `0`)] - ])) as map; - - - const metadata : bytes = bytes - `{ - "name":"FA2 NFT Marketplace", - "description":"Example of FA2 implementation", - "version":"0.0.1", - "license":{"name":"MIT"}, - "authors":["Marigold"], - "homepage":"https://marigold.dev", - "source":{ - "tools":["Ligo"], - "location":"https://github.com/ligolang/contract-catalogue/tree/main/lib/fa2"}, - "interfaces":["TZIP-012"], - "errors": [], - "views": [] - }` ; - - return [list([]) as list, - {...s, - ledger: Big_map.add([Tezos.get_sender(),token_id],quantity as nat,s.ledger) as MULTIASSET.Ledger.t, - metadata : Big_map.literal(list([["", bytes `tezos-storage:data`],["data", metadata]])), - token_metadata: Big_map.add(token_id, {token_id: token_id,token_info:token_info},s.token_metadata), - operators: Big_map.empty as MULTIASSET.Operators.t, - owner_token_ids : Set.add([Tezos.get_sender(),token_id],s.owner_token_ids), - token_ids: Set.add(token_id, s.token_ids)}]}; +@entry +const mint = ( + [token_id, quantity, name, description, symbol, ipfsUrl]: [ + nat, + nat, + bytes, + bytes, + bytes, + bytes + ], + s: storage +): ret => { + if (quantity <= (0 as nat)) return failwith("0"); + if (! Set.mem(Tezos.get_sender(), s.administrators)) return failwith("1"); + const token_info: map = + Map.literal( + list( + [ + ["name", name], + ["description", description], + ["interfaces", (bytes `["TZIP-12"]`)], + ["artifactUri", ipfsUrl], + ["displayUri", ipfsUrl], + ["thumbnailUri", ipfsUrl], + ["symbol", symbol], + ["decimals", (bytes `0`)] + ] + ) + ) as map; + return [ + list([]) as list, + { + ...s, + ledger: Big_map.add( + [Tezos.get_sender(), token_id], + quantity as nat, + s.ledger + ) as FA2Impl.Datatypes.ledger, + token_metadata: Big_map.add( + token_id, + { token_id: token_id, token_info: token_info }, + s.token_metadata + ), + operators: Big_map.empty as FA2Impl.Datatypes.operators + } + ] +}; ``` You also need to update `sell` function ```ligolang -const sell = (token_id : nat, quantity: nat, price: nat, s: storage) : ret => { - +@entry +const sell = ([token_id, quantity, price]: [nat, nat, nat], s: storage): ret => { //check balance of seller - const sellerBalance = MULTIASSET.Ledger.get_for_user(s.ledger,Tezos.get_source(),token_id); - if(quantity > sellerBalance) return failwith("2"); + const sellerBalance = + FA2Impl.Sidecar.get_for_user([s.ledger, Tezos.get_source(), token_id]); + if (quantity > sellerBalance) return failwith("2"); //need to allow the contract itself to be an operator on behalf of the seller - const newOperators = MULTIASSET.Operators.add_operator(s.operators,Tezos.get_source(),Tezos.get_self_address(),token_id); + const newOperators = + FA2Impl.Sidecar.add_operator( + [s.operators, Tezos.get_source(), Tezos.get_self_address(), token_id] + ); //DECISION CHOICE: if offer already exists, we just override it - return [list([]) as list,{...s,offers:Map.add([Tezos.get_source(),token_id],{quantity : quantity, price : price},s.offers),operators:newOperators}]; + + return [ + list([]) as list, + { + ...s, + offers: Map.add( + [Tezos.get_source(), token_id], + { quantity: quantity, price: price }, + s.offers + ), + operators: newOperators + } + ] }; ``` Same for the `buy` function ```ligolang -const buy = (token_id : nat, quantity: nat, seller: address, s: storage) : ret => { - +@entry +const buy = ([token_id, quantity, seller]: [nat, nat, address], s: storage): ret => { //search for the offer - return match( Map.find_opt([seller,token_id],s.offers) , { - None : () => failwith("3"), - Some : (offer : offer) => { - - //check if amount have been paid enough - if(Tezos.get_amount() < offer.price * (1 as mutez)) return failwith("5"); - - // prepare transfer of XTZ to seller - const op = Tezos.transaction(unit,offer.price * (1 as mutez),Tezos.get_contract_with_error(seller,"6")); - - //transfer tokens from seller to buyer - let ledger = MULTIASSET.Ledger.decrease_token_amount_for_user(s.ledger,seller,token_id,quantity); - ledger = MULTIASSET.Ledger.increase_token_amount_for_user(ledger,Tezos.get_source(),token_id,quantity); - //update new offer - const newOffer = {...offer,quantity : abs(offer.quantity - quantity)}; + return match(Map.find_opt([seller, token_id], s.offers)) { + when (None()): + failwith("3") + when (Some(offer)): + do { + //check if amount have been paid enough - return [list([op]) as list, {...s, offers : Map.update([seller,token_id],Some(newOffer),s.offers), ledger : ledger, owner_token_ids : Set.add([Tezos.get_source(),token_id],s.owner_token_ids) }]; - } - }); -}; -``` - -and finally the `main` function + if (Tezos.get_amount() < offer.price * (1 as mutez)) return failwith( + "5" + ); + // prepare transfer of XTZ to seller -```ligolang -const main = ([p, s]: [parameter, storage]): ret => - match( - p, - { - Mint: (p: [nat, nat, bytes, bytes, bytes, bytes]) => - mint(p[0], p[1], p[2], p[3], p[4], p[5], s), - AddAdministrator: (p: address) => { - if (Set.mem(Tezos.get_sender(), s.administrators)) { - return [ - list([]), - { ...s, administrators: Set.add(p, s.administrators) } - ] - } else { - return failwith("1") - } - }, - Buy: (p: [nat, nat, address]) => buy(p[0], p[1], p[2], s), - Sell: (p: [nat, nat, nat]) => sell(p[0], p[1], p[2], s), - Transfer: (p: MULTIASSET.transfer) => { - const ret2: [list, MULTIASSET.storage] = - MULTIASSET.transfer( - [ - p, - { - ledger: s.ledger, - metadata: s.metadata, - token_metadata: s.token_metadata, - operators: s.operators, - owner_token_ids: s.owner_token_ids, - token_ids: s.token_ids - } - ] + const op = + Tezos.transaction( + unit, + offer.price * (1 as mutez), + Tezos.get_contract_with_error(seller, "6") ); - return [ - ret2[0], - { - ...s, - ledger: ret2[1].ledger, - metadata: ret2[1].metadata, - token_metadata: ret2[1].token_metadata, - operators: ret2[1].operators, - owner_token_ids: ret2[1].owner_token_ids, - token_ids: ret2[1].token_ids - } - ] - }, - Balance_of: (p: MULTIASSET.balance_of) => { - const ret2: [list, MULTIASSET.storage] = - MULTIASSET.balance_of( - [ - p, - { - ledger: s.ledger, - metadata: s.metadata, - token_metadata: s.token_metadata, - operators: s.operators, - owner_token_ids: s.owner_token_ids, - token_ids: s.token_ids - } - ] + //transfer tokens from seller to buyer + + let ledger = + FA2Impl.Sidecar.decrease_token_amount_for_user( + [s.ledger, seller, token_id, quantity] ); - return [ - ret2[0], - { - ...s, - ledger: ret2[1].ledger, - metadata: ret2[1].metadata, - token_metadata: ret2[1].token_metadata, - operators: ret2[1].operators, - owner_token_ids: ret2[1].owner_token_ids, - token_ids: ret2[1].token_ids - } - ] - }, - Update_operators: (p: MULTIASSET.update_operators) => { - const ret2: [list, MULTIASSET.storage] = - MULTIASSET.update_ops( - [ - p, - { - ledger: s.ledger, - metadata: s.metadata, - token_metadata: s.token_metadata, - operators: s.operators, - owner_token_ids: s.owner_token_ids, - token_ids: s.token_ids - } - ] + ledger + = FA2Impl.Sidecar.increase_token_amount_for_user( + [ledger, Tezos.get_source(), token_id, quantity] ); + //update new offer + + const newOffer = { ...offer, quantity: abs(offer.quantity - quantity) }; return [ - ret2[0], + list([op]) as list, { ...s, - ledger: ret2[1].ledger, - metadata: ret2[1].metadata, - token_metadata: ret2[1].token_metadata, - operators: ret2[1].operators, - owner_token_ids: ret2[1].owner_token_ids, - token_ids: ret2[1].token_ids + offers: Map.update([seller, token_id], Some(newOffer), s.offers), + ledger: ledger } ] } - } - ); + } +}; ``` Change the initial storage to ```ligolang -#include "nft.jsligo" -const default_storage = -{ - administrators: Set.literal(list(["tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb" as address])) as set
, - offers: Map.empty as map<[address,nat],offer>, - ledger: Big_map.empty as MULTIASSET.Ledger.t, - metadata: Big_map.empty as MULTIASSET.Metadata.t, - token_metadata: Big_map.empty as MULTIASSET.TokenMetadata.t, - operators: Big_map.empty as MULTIASSET.Operators.t, - owner_token_ids : Set.empty as set<[MULTIASSET.Storage.owner,MULTIASSET.Storage.token_id]>, - token_ids : Set.empty as set - } -; +#import "nft.jsligo" "Contract" + +const default_storage: Contract.storage = { + administrators: Set.literal( + list(["tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb" as address]) + ) as set
, + offers: Map.empty as map<[address, nat], Contract.offer>, + ledger: Big_map.empty as Contract.FA2Impl.MultiAsset.ledger, + metadata: Big_map.literal( + list( + [ + ["", bytes `tezos-storage:data`], + [ + "data", + bytes + `{ + "name":"FA2 NFT Marketplace", + "description":"Example of FA2 implementation", + "version":"0.0.1", + "license":{"name":"MIT"}, + "authors":["Marigold"], + "homepage":"https://marigold.dev", + "source":{ + "tools":["Ligo"], + "location":"https://github.com/ligolang/contract-catalogue/tree/main/lib/fa2"}, + "interfaces":["TZIP-012"], + "errors": [], + "views": [] + }` + ] + ] + ) + ) as Contract.FA2Impl.TZIP16.metadata, + token_metadata: Big_map.empty as Contract.FA2Impl.TZIP12.tokenMetadata, + operators: Big_map.empty as Contract.FA2Impl.MultiAsset.operators, +}; ``` Compile again and deploy to ghostnet ```bash -TAQ_LIGO_IMAGE=ligolang/ligo:0.64.2 taq compile nft.jsligo +TAQ_LIGO_IMAGE=ligolang/ligo:1.0.0 taq compile nft.jsligo taq deploy nft.tz -e "testing" ``` @@ -300,11 +247,11 @@ taq deploy nft.tz -e "testing" β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Contract β”‚ Address β”‚ Alias β”‚ Balance In Mutez β”‚ Destination β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ nft.tz β”‚ KT1QfMdyRq56xLBiofFTjLhkq5VCdj9PwC25 β”‚ nft β”‚ 0 β”‚ https://ghostnet.ecadinfra.com β”‚ +β”‚ nft.tz β”‚ KT1KAkKJdbx9FGwYhKfWN3pHovX1mb3fQpC4 β”‚ nft β”‚ 0 β”‚ https://ghostnet.ecadinfra.com β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` -** Hooray! We have finished the smart contract _(backend)_ ** +**The smart contract _(backend)_ is finished** ## NFT Marketplace front @@ -314,12 +261,12 @@ Generate Typescript classes and go to the frontend to run the server taq generate types ./app/src cd ./app yarn install -yarn run start +yarn dev ``` ## Update in `App.tsx` -We forget about `token_id == 0` and fetch back all tokens. +Forget about `token_id == 0` and fetch back all tokens. Replace the function `refreshUserContextOnPageReload` with the following content ```typescript @@ -334,12 +281,23 @@ const refreshUserContextOnPageReload = async () => { nftContractAddress ); const storage = (await nftContrat.storage()) as Storage; + + const token_metadataBigMapId = ( + storage.token_metadata as unknown as { id: BigNumber } + ).id.toNumber(); + + const token_ids = await api.bigMapsGetKeys(token_metadataBigMapId, { + micheline: "Json", + active: true, + }); await Promise.all( - storage.token_ids.map(async (token_id: nat) => { + token_ids.map(async (token_idKey) => { + const key: string = token_idKey.key; + let tokenMetadata: TZIP21TokenMetadata = (await c .tzip12() - .getTokenMetadata(token_id.toNumber())) as TZIP21TokenMetadata; - nftContratTokenMetadataMap.set(token_id.toNumber(), tokenMetadata); + .getTokenMetadata(Number(key))) as TZIP21TokenMetadata; + nftContratTokenMetadataMap.set(key, tokenMetadata); }) ); setNftContratTokenMetadataMap(new Map(nftContratTokenMetadataMap)); //new Map to force refresh @@ -459,11 +417,11 @@ export default function MintPage() { useEffect(() => { (async () => { - if (storage && storage.token_ids.length > 0) { - formik.setFieldValue("token_id", storage?.token_ids.length); + if (nftContratTokenMetadataMap && nftContratTokenMetadataMap.size > 0) { + formik.setFieldValue("token_id", nftContratTokenMetadataMap.size); } })(); - }, [storage?.token_ids]); + }, [nftContratTokenMetadataMap?.size]); const mint = async ( newTokenDefinition: TZIP21TokenMetadata & { quantity: number } @@ -477,11 +435,11 @@ export default function MintPage() { const requestHeaders: HeadersInit = new Headers(); requestHeaders.set( "pinata_api_key", - `${process.env.REACT_APP_PINATA_API_KEY}` + `${import.meta.env.VITE_PINATA_API_KEY}` ); requestHeaders.set( "pinata_secret_api_key", - `${process.env.REACT_APP_PINATA_API_SECRET}` + `${import.meta.env.VITE_PINATA_API_SECRET}` ); const resFile = await fetch( @@ -812,6 +770,7 @@ Copy the content below, and paste it to `OffersPage.tsx` ```typescript import { InfoOutlined } from "@mui/icons-material"; import SellIcon from "@mui/icons-material/Sell"; +import * as api from "@tzkt/sdk-api"; import { Box, @@ -859,15 +818,17 @@ type Offer = { }; export default function OffersPage() { + api.defaults.baseUrl = "https://api.ghostnet.tzkt.io"; + const [selectedTokenId, setSelectedTokenId] = React.useState(0); const [currentPageIndex, setCurrentPageIndex] = useState(1); - let [offersTokenIDMap, setOffersTokenIDMap] = React.useState>( - new Map() - ); - let [ledgerTokenIDMap, setLedgerTokenIDMap] = React.useState>( - new Map() - ); + let [offersTokenIDMap, setOffersTokenIDMap] = React.useState< + Map + >(new Map()); + let [ledgerTokenIDMap, setLedgerTokenIDMap] = React.useState< + Map + >(new Map()); const { nftContrat, @@ -901,27 +862,38 @@ export default function OffersPage() { ledgerTokenIDMap = new Map(); offersTokenIDMap = new Map(); + const ledgerBigMapId = ( + storage.ledger as unknown as { id: BigNumber } + ).id.toNumber(); + + const owner_token_ids = await api.bigMapsGetKeys(ledgerBigMapId, { + micheline: "Json", + active: true, + }); + await Promise.all( - storage.owner_token_ids.map(async (element) => { - if (element[0] === userAddress) { + owner_token_ids.map(async (owner_token_idKey) => { + const key: { address: string; nat: string } = owner_token_idKey.key; + + if (key.address === userAddress) { const ownerBalance = await storage.ledger.get({ 0: userAddress as address, - 1: element[1], + 1: BigNumber(key.nat) as nat, }); - if (ownerBalance != BigNumber(0)) - ledgerTokenIDMap.set(element[1], ownerBalance); + if (ownerBalance.toNumber() !== 0) + ledgerTokenIDMap.set(Number(key.nat), ownerBalance); const ownerOffers = await storage.offers.get({ 0: userAddress as address, - 1: element[1], + 1: BigNumber(key.nat) as nat, }); - if (ownerOffers && ownerOffers.quantity != BigNumber(0)) - offersTokenIDMap.set(element[1], ownerOffers); + if (ownerOffers && ownerOffers.quantity.toNumber() !== 0) + offersTokenIDMap.set(Number(key.nat), ownerOffers); console.log( "found for " + - element[0] + + key.address + " on token_id " + - element[1] + + key.nat + " with balance " + ownerBalance ); @@ -1032,7 +1004,7 @@ export default function OffersPage() { {"Description : " + nftContratTokenMetadataMap.get( - token_id.toNumber() + token_id.toString() )?.description} @@ -1042,7 +1014,7 @@ export default function OffersPage() { } title={ - nftContratTokenMetadataMap.get(token_id.toNumber())?.name + nftContratTokenMetadataMap.get(token_id.toString())?.name } /> { - setSelectedTokenId(token_id.toNumber()); + setSelectedTokenId(token_id); formik.handleSubmit(values); }} > @@ -1297,7 +1269,7 @@ export default function WineCataloguePage() { page={currentPageIndex} onChange={(_, value) => setCurrentPageIndex(value)} count={Math.ceil( - Array.from(storage?.offers.entries()).filter(([key, offer]) => + Array.from(storage?.offers.entries()).filter(([_, offer]) => offer.quantity.isGreaterThan(0) ).length / itemPerPage )} @@ -1308,7 +1280,7 @@ export default function WineCataloguePage() { cols={isDesktop ? itemPerPage / 2 : isTablet ? itemPerPage / 3 : 1} > {Array.from(storage?.offers.entries()) - .filter(([key, offer]) => offer.quantity.isGreaterThan(0)) + .filter(([_, offer]) => offer.quantity.isGreaterThan(0)) .filter((_, index) => index >= currentPageIndex * itemPerPage - itemPerPage && index < currentPageIndex * itemPerPage @@ -1329,7 +1301,7 @@ export default function WineCataloguePage() { {"Description : " + nftContratTokenMetadataMap.get( - key[1].toNumber() + key[1].toString() )?.description} {"Seller : " + key[0]} @@ -1340,7 +1312,7 @@ export default function WineCataloguePage() { } title={ - nftContratTokenMetadataMap.get(key[1].toNumber())?.name + nftContratTokenMetadataMap.get(key[1].toString())?.name } /> . -You can verify that they are installed by running these commands: - - ```bash - node --version - npm --version - ``` - - If you see a message with the versions of Node.JS and NPM, they are installed correctly. - -- To install Docker Desktop, see . -Make sure to start Docker Desktop after you install it. - -- To install the `tznft` tool, run this command: - - ```bash - npm install -g @oxheadalpha/tznft - ``` - - You can verify that it is installed by running this command: - - ```bash - tznft --version - ``` - - If you see a message with the version of the `tznft` tool, it is installed correctly. - -## Create a project folder - -1. Create a folder to store your NFT configuration files: - - ```bash - mkdir nft-tutorial - cd nft-tutorial - ``` - -3. Create a starter NFT configuration file: - - ```bash - tznft init - ``` - - The resulting file, named `tznft.config`, contains information about the Tezos networks that are available for you to work with, including the [Ghostnet](https://teztnets.xyz/ghostnet-about) test network and the local sandbox that you set up in the next steps. - The `tznft` tool requires this file, so the commands in the following steps work only from the directory that you ran `tznft init` in. - -4. Check that the default active network is "sandbox:" - - ```bash - tznft show-network - ``` - - The response should show that the active network is "sandbox." - The sandbox is a local simulation of Tezos that you can use to test your work. - -5. Set up a local Tezos sandbox by running this command: - - ```bash - tznft bootstrap - ``` - - This command can take time to run, so wait until you see the message "sandbox started." - - This command uses the [Flextesa](https://tezos.gitlab.io/flextesa/) tool to create a local sandbox in a Docker container. - This sandbox is a local instance of Tezos that you can use to test your work before you send it to a live Tezos network. - The sandbox comes preconfigured with two account aliases named `bob` and `alice` that you can use to test account operations like creating and transferring NFTs. - - You can verify that the sandbox is running by running the command `docker ps` and looking for a container named `flextesa-sandbox`. - To stop the container, run the command `tznft kill-sandbox`, but beware that stopping the container sets the sandbox back to its initial state, which removes any changes you made or contracts or tokens that you created. - - Unlike the live Tezos networks, this sandbox bakes a new block every 5 seconds. - Therefore, commands that you run on the sandbox can take a few seconds to complete. - -## Create NFT metadata - -The first step in creating NFTs is to create local metadata files that describe the collection and the individual NFTs: - -1. Create a collection metadata file by running this command: - - ```bash - tznft create-collection-meta my_collection - ``` - - The new metadata file is named `my_collection.json` and has information such as the name, description, home page, and creator of the collection. - It also includes the interfaces that the NFTs support, including the TZIP-12 interface that was mentioned earlier. - -1. Optional: Edit the `my_collection.json` file to put your information in the `name`, `description`, and `authors` fields. - -1. Validate the collection by running this command: - - ```bash - tznft validate-collection-meta my_collection.json - ``` - - If you did not change values in the file, this command may show warnings that the collection uses placeholder values. - You can continue with these placeholder values or insert your own information. - If there are any errors, make sure that the file is valid JSON. - -1. Create a metadata file for the first NFT in the collection by running this command: - - ```bash - tznft create-nft-meta Token1 bob ipfs://QmRyTc9KbD7ZSkmEf4e7fk6A44RPciW5pM4iyqRGrhbyvj - ``` - - This command creates a metadata file named `Token1.json` with default information about the NFT. - It includes the minter's account address and URIs to pictures that represent the NFT. - In this case, the `ipfs` URI links to a picture of the Tezos logo, which you can see at this link: . - -1. Optional: Edit the metadata such as the name and description fields in the `Token1.json` file. - -1. Optional: Edit other fields in the metadata based on the FA2 standard. - - For example, you can expand the `attributes` section with other attributes. - Each attribute must have the `name` and `value` fields and can optionally have a `type` field, as in this example: - - ```json - "attributes": [ - { - "name": "My string attribute", - "value": "String attribute value" - }, - { - "name": "My integer attribute", - "value": "5", - "type": "integer" - }, - { - "name": "My number attribute", - "value": "12.3", - "type": "number" - }, - { - "name": "My percentage attribute", - "value": "19", - "type": "percentage" - } - ] - ``` - - By default the `artifactUri`, `displayUri`, and `thumbnailUri` fields are set to the picture that you passed in the `tznft create-nft-meta` command. - You can update these to different images to allow applications to show media to represent the NFT. - You can also add a `formats` object to provide media in different formats, such as different image, video, or audio formats: - - ```json - "formats": [ - { - "uri": "ipfs://QmRyTc9KbD7ZSkmEf4e7fk6A44RPciW5pM4iyqRGrhbyvj", - "hash": "a56017a1317b1bc900acdaf600874c00e5c048d30894f452049db6dcef6e4f0d", - "mimeType": "image/svg+xml" - }, - { - "uri": "ipfs://QmRyTc9KbD7ZSkmEf4e7fk6A44RPciW5pM4iyqRGrhbyvj", - "hash": "8968db6bde43255876c464613a31fbd0416ca7d74be4c5ae86c1450418528302", - "mimeType": "image/png", - "dimensions": { - "value": "512x512", - "unit": "px" - } - }, - { - "uri": "ipfs://QmRyTc9KbD7ZSkmEf4e7fk6A44RPciW5pM4iyqRGrhbyvj", - "hash": "d4a93fc8d8991caa9b52c04c5ff7edf5c4bc29317a373e3a97f1398c697d6714", - "mimeType": "model/gltf+json" - } - ] - ``` - - For specifics about what is allowed in an NFT metadata file, see the [TZIP-21](https://gitlab.com/tezos/tzip/-/blob/master/proposals/tzip-21/tzip-21.md) standard. - -1. Validate the NFT metadata file with this command: - - ```bash - tznft validate-nft-meta Token1.json - ``` - - If the file does not validate, verify that it is valid JSON and has only the fields listed in the [TZIP-21](https://gitlab.com/tezos/tzip/-/blob/master/proposals/tzip-21/tzip-21.md) standard. - -1. Create at least one more metadata file for other NFTs by running commands like this example: - - ```bash - tznft create-nft-meta Token2 bob ipfs://QmRyTc9KbD7ZSkmEf4e7fk6A44RPciW5pM4iyqRGrhbyvj - ``` - -## Configure IPFS storage - -Because storage space on blockchains is expensive, developers don't put entire token metadata files on Tezos. -Instead, they configure decentralized storage for the NFT data and put only the link to that data on Tezos itself. -In this section, you set up storage for the NFT metadata using the InterPlanetary File System (IPFS) protocol. - -IPFS requires authentication just like blockchain transactions, so in this section you set up an account with the Pinata IPFS provider and use it to upload (or _pin_) the NFT data to IPFS. - -1. Create a free Pinata account at . - -1. Go to the API Keys tab and click **New Key**. - -1. On the Create New API Key page, expand API Endpoint Access and enable the `pinFileToIPFS` permission, as in this picture: - - ![Selecting the permissions for the Pinata key](/images/nft-create/pinata-key-permissions.png) - -1. In the **Key Name** field, give the key a name, such as "My Key." - -1. Click **Create Key**. - - The API Key Info window shows the API key and secret, which you must copy immediately, because it is not shown again. - -1. Copy the API Key and API Secret fields and save the values on your computer. -You need these values in the next section. - - You can see the new API key on the API Keys tab: - - ![The new Pinata API key in the Pinata web app](/images/nft-create/created-pinata-key.png) - -1. Add the API key and secret to your local `tznft` configuration by running this command, replacing `$PINATA_KEY` and `$PINATA_SECRET` with your API key and secret: - - ```bash - tznft set-pinata-keys $PINATA_KEY $PINATA_SECRET --force - ``` - - This command stores the key and secret in the `tznft.json` file, so be careful not to share this file. - -1. Pin the first NFT metadata file to IPFS by running this command and passing the file name and a tag for the NFT, which can be the same as the file name: - - ```bash - tznft pin-file Token1.json --tag Token1 - ``` - - The command returns the URI for the data on IPFS, which starts with `ipfs://`. - -1. Copy the IPFS URI, because you will need it later. - -1. In the same way, pin the other NFT metadata files with the `tznft pin-file` command and save their URIs. - -1. Optional: Verify that the files are pinned successfully by opening the Pinata app to the Files page, as in this picture: - - ![The Files tab on Pinata, showing three NFT metadata files](/images/nft-create/pinned-nft-meta.png) - -Now that the metadata is pinned to IPFS, you can create NFTs that link to this metadata. - -## Mint NFTs - -Creating NFTs is called _minting_. -First, you create the smart contract to manage the NFTs. -Then, you mint one or more NFTs with that contract. -The related `tznft` commands use the configuration files that you created earlier. - -1. Create the collection contract from the metadata file by running this command: - - ```bash - tznft create-collection bob --meta_file my_collection.json --alias my_collection - ``` - - This command takes the alias of the user who is the owner of the collection. - In this case, the owner is one of the default accounts in the sandbox. - The command also includes the metadata file and an optional local alias for the collection. - - The command also updates the `tznft.json` file with information about the new collection, including the address of the smart contract that manages the collection. - This smart contract is a pre-compiled FA2 NFT contract written in the [LIGO](https://ligolang.org/) smart contract language. - You can write your own smart contracts to manage NFTs, but using this contract prevents errors and provides all of the functionality needed to create, transfer, and manage NFTs. - -1. Run this command to create a token and set Bob as the owner, replacing the IPFS URI with the URI that the `tznft pin-file` command returned in the previous section: - - ```bash - tznft mint bob my_collection --tokens '1, ipfs://abcde12345' - ``` - - This command includes these parameters: - - - The alias or address of the initial owner. - - The alias of the collection from the `tznft create-collection` command. - - The ID number and IPFS URI for the NFTs in a comma-delimited string. - - If you forgot the IPFS URI, you can look it up in the Pinata app on the Files tab. - This tab has a column labeled "Content Identifier (CID)." - To create the IPFS URI, add the content identifier to the string `ipfs://`. - - The response in the terminal says that the token was minted. - -1. Run the `tznft mint` command to mint the other NFTs. -You can create more than one NFT in a single command by providing more than one string after the `--tokens` switch, as in this example: - - ```bash - tznft mint bob my_collection --tokens '2, ipfs://defgh12345' '3, ipfs://ijklm12345' - ``` - -1. Verify that the NFTs were minted successfully by getting their metadata with the `tznft show-meta` command: - - ```bash - tznft show-meta bob --nft my_collection --tokens 1 2 - ``` - - If the NFTs were created successfully, the command prints the metadata that you pinned to IPFS. - -Now the NFTs are minted to the sandbox. -Because these NFTs are only on your local computer, in the Flextesa sandbox, you can interact with them only locally. -They exist as long as you keep the Flextesa Docker container running, which you started with the `tznft bootstrap` command. - -## Transferring and manipulating NFTs - -The `tznft` command provides commands to manipulate NFTs locally, including transferring them between accounts. -Just like transactions on live blockchain networks, the transaction signer must have permission to transfer or manipulate the NFTs. -Currently, only Bob has access to the NFTs, so the `tznft` commands include him as the signer of most transactions. - -1. Use the `tznft show-balance` command to print information about Bob's NFTs. -This command takes the alias or address of the collection, the signer of the transaction, the owner of the NFTs, and the IDs of one or more NFTs. - - ```bash - tznft show-balance --nft my_collection --signer bob --owner bob --tokens 1 2 - ``` - - Because NFTs are unique, the response shows a balance of 1 if the account owns the token and 0 if it does not, as in this picture: - - ![THe results of the `show-balance` command, with two NFTs in Bob's account](/images/nft-create/show-balance-bob.png) - -1. Use the `tznft show-balance` command to print information about Alice's NFTs: - - ```bash - tznft show-balance --nft my_collection --signer alice --owner alice --tokens 1 2 - ``` - - Because Bob is the initial owner of all of the NFTs, Alice's balance is 0 for each NFT. - -1. Use the `tznft transfer` command to transfer one or more NFTs from Bob to Alice. -This command takes the alias or address of the collection, the signer, and one or more comma-separated strings with the current owner, the new owner, and the ID of the NFT to transfer. -For example, this command transfers NFTs 1 and 2 from Bob to Alice: - - ```bash - tznft transfer --nft my_collection --signer bob --batch 'bob, alice, 1' 'bob, alice, 2' - ``` - -1. Verify that the transfer worked by checking Alice's balance with the `tznft show-balance` command: - - ```bash - tznft show-balance --nft my_collection --signer alice --owner alice --tokens 1 2 - ``` - - Now Alice's balance is 1 for each token that transferred. - Alice is in control of these NFTs and Bob can no longer transfer them. - -1. Verify that Bob does not have control over the transferred NFTs by trying to transfer them back from Alice's account to Bob's account: - - ```bash - tznft transfer --nft my_collection --signer bob --batch 'alice, bob, 1' 'alice, bob, 2' - ``` - - The response shows the error "FA2_NOT_OPERATOR" because Bob's account is not in control of these NFTs. - - You can give Bob's account control over the NFTs by making his account an operator of those NFTs. - -1. Make Bob an operator of Alice's NFTs by passing the token IDs to the `tznft update-ops` command: - - ```bash - tznft update-ops alice --nft my_collection --add 'bob, 1' 'bob, 2' - ``` - -1. Try again to transfer the NFTs from Alice's account to Bob's account with a transaction signed by Bob: - - ```bash - tznft transfer --nft my_collection --signer bob --batch 'alice, bob, 1' 'alice, bob, 2' - ``` - -1. Check Bob's account to see that he now owns the NFTs: - - ```bash - tznft show-balance --nft my_collection --signer bob --owner bob --tokens 1 2 - ``` - -## Freeze the collection - -When you have created all of the NFTs that you want, freeze the collection so it cannot be changed and no more NFTs can be added by running this command: - -```bash -tznft mint-freeze bob my_collection -``` - -## Mint tokens on a testnet - -So far, the NFTs that you have created are available only in your local sandbox. -When you are satisfied with the NFTs and how they behave, you can send them to a testnet and test them there. -You can use the same configuration files and IPFS data as you used on the sandbox. - -By default, the `tznft.json` file has configuration information for the Tezos Ghostnet testnet, where you can test your tokens on a running Tezos network. - -1. Show the available networks by running the command `tznft show-network --all` and verify that the testnet is in the list. - -1. Change the `tznft` tool to use the testnet instead of your local sandbox: - - ```bash - tznft set-network testnet - ``` - -1. Run the `tznft bootstrap` command to get the testnet ready for your use. -Now that the network is set to testnet, this command deploys a helper balance inspector contract to testnet that allows the `tznft` command to get information from the testnet. -You only need to run this command for testnet once. - -1. Create an alias on the testnet to own the NFTs. -You can do this in either of these two ways: - - - If you have an existing Tezos wallet that supports testnets (such as Temple wallet), copy the private key from that wallet and use the `tznft add-alias` command to create a local alias for it. - For example, this command creates a wallet with the alias `my-account`: - - ```bash - tznft add-alias my-account $TEZOS_PRIVATE_KEY - ``` - - - Create a local wallet with the installation of the `octez-client` command within the Flextesa Docker container: - - 1. Generate local keys with the `octez-client gen keys` command. - For example, this command creates keys for a wallet with the alias `my-account`: - - ```bash - docker exec flextesa-sandbox octez-client gen keys my-account - ``` - - 1. Get the keys for the wallet with this command: - - ```bash - docker exec flextesa-sandbox octez-client show address my-account -S - ``` - - The response includes the hash, public key, and secret key for the wallet. - For example, in this response, the secret key starts with "edsk3WR": - - ![The keys for the new account](/images/nft-create/new-key-output.png) - - 1. Add the secret key as an alias with the `tznft` command, replacing `$TEZOS_PRIVATE_KEY` with the value of the secret key from the previous command: - - ```bash - tznft add-alias my-account $TEZOS_PRIVATE_KEY - ``` - - 1. Add funds to the new wallet by going to the Ghostnet faucet at , pasting the wallet's hash in the "Or fund any address" field, and clicking a button to request tokens. - The wallet needs tokens to pay the fees to create the collection and mint the tokens on Ghostnet. - -1. Create the collection on the testnet. -The command is the same as for the sandbox, and you can create a new collection file or use the file from the sandbox. -Similarly, you can use the same collection alias because `tznft` keeps aliases separate on different networks, but be sure not to get the aliases confused. - - ```bash - tznft create-collection my-account --meta_file my_collection.json --alias my_collection - ``` - -1. Mint the tokens on the testnet. -The command is the same as for the sandbox: - - ```bash - tznft mint my-account my_collection --tokens '1, ipfs://abcde12345' - ``` - - You can add more NFTs until you freeze the collection. - -1. View your token balances. -The command is the same as for the sandbox: - - ```bash - tznft show-balance --nft my_collection --signer my-account --owner my-account --tokens 1 - ``` - -1. View the tokens on a block explorer: - - 1. Get the address of the collection on the testnet from the `testnet` section of the `tznft.json` file. - The address starts with "KT1". - - 1. Go to a block explorer, such as . - - 1. Set the block explorer to use testnet instead of Tezos mainnet. - - 1. In the search field, paste the address of the collection and press Enter. - - The block explorer shows information about the contract that manages the NFTs, including a list of all NFTs in the contract, who owns them, and a list of recent transactions. - -Now the NFTs are on Tezos ghostnet and you can transfer and manipulate them just like you did in the sandbox. -You may need to create and fund more account aliases to transfer them, but the commands are the same. -For example, to transfer NFTs to an account with the alias `other-account`, run this command: - -```bash -tznft transfer --nft my_collection --signer my-account --batch 'my-account, other-account, 1' 'my-account, other-account, 2' -``` - -## Summary - -Now you can create, test, and deploy NFTs locally and to testnets. -The process for minting NFTs to Tezos mainnet is the same, but you must use an account with real XTZ in it to pay the transaction fees. - -If you want to continue working with these NFTs, try creating a marketplace for them as described in the tutorial [Build an NFT Marketplace](../build-an-nft-marketplace). +- The tutorial [Create a web app that mints NFTs](./nft-taquito/) covers how to create a web application that allows users to mint their own NFTs using the [Taquito](https://tezostaquito.io/) JavaScript/TypeScript SDK for Tezos. diff --git a/src/pages/tutorials/create-an-nft/nft-pinata/index.md b/src/pages/tutorials/create-an-nft/nft-pinata/index.md deleted file mode 100644 index 7ed0db410..000000000 --- a/src/pages/tutorials/create-an-nft/nft-pinata/index.md +++ /dev/null @@ -1,258 +0,0 @@ ---- -id: nft-pinata -title: Mint an NFT using Taquito and Pinata -lastUpdated: 10th July 2023 ---- - -In this guide, you will get an overview of the mechanics of NFTs, in general, and on Tezos. If you want to get your hands dirty, you will get an explanation of how the code of an NFT platform works, on the contract level but also on the app level. This article is mainly designed for beginner programmers who want to get a better idea of the code involved in the creation and use of NFTs, but non-technical readers will also find important information to deepen their understanding of both NFTs and the Tezos blockchain. - -You will learn how to build a simple NFT platform backed by a smart contract on the Tezos blockchain capable of minting, transferring, and burning NFTs while hosting their metadata on the IPFS. You need a basic knowledge of JavaScript to follow along in part 2. We will only have a high-level overview of the smart contract so no knowledge of Ligo is required, but a general knowledge of programming concepts would help. - -If you just want the code, you can find the complete source code in [this GitHub repository](https://github.com/claudebarde/taquito-pinata-tezos-nft). The **backend** folder holds the code that generates the metadata and pins the picture and the metadata to the IPFS. The **frontend** folder holds the code for the app that allows users to connect to their Tezos wallet and mint NFTs. The **contract** folder holds the FA2 contract written in CameLigo to mint, transfer, and burn NFTs. - -Now, let’s start by understanding better what NFTs are and how they work! - -> Note: the first part of the article doesn’t require any knowledge in programming but to follow the second part, you need to have a basic knowledge of JavaScript. - - -## What is an NFT? - -β€œNFT” is an acronym that stands for β€œnon-fungible token”. A token is called β€œfungible” when you can exchange it for another identical token without losing any value. For example, the coins in your wallet are fungible. If we both have 1 euro and we exchange our coins, we both still have 1 euro. An NFT is non-fungible due to its unique nature: there is no other token 100% identical with the same value. - -> Note: the word β€œtoken” in this context means β€œa list of data that altogether represent something”. An NFT is a token because it is made of data related to something from the real world. - -Although NFTs are mostly known for representing artwork, they can actually represent anything and have been used to represent certificates, contracts, users, etc. One of the most important things to remember about NFTs is that they are not a β€œphysical object” but a representation of somebody’s ownership. When you own the NFT for an artwork, it doesn’t prove you have the artwork with you, but it proves you have ownership of the artwork. - -On a technical level, NFTs are stored in smart contracts, little programs that live on blockchains and that are able to execute and store data. This point is particularly important to remember: if you have an NFT on your favorite platform, the data is saved in the contract the platform is built upon and the other platforms in the ecosystem built on different contracts are not aware of your NFT. In most cases, this also means you cannot transfer your NFT from one platform to another. If you are the creator of the NFT, you can β€œ_burn_” it \(i.e destroy it\) and β€œ_mint_” it \(i.e create it\) on another platform. - -On Tezos, NFTs are stored in contracts that follow the [TZIP-12 standard](https://gitlab.com/tzip/tzip/-/blob/master/proposals/tzip-12/tzip-12.md) that you will often see labeled as β€œ**FA2 contracts**”. The NFT is made of 2 main parts: an id to identify it in the contract and metadata to describe what the NFT is. The contract holds a _ledger_, a table where the ids of every NFT are matched with the address of their owner. Another table in the contract matches the ids of every NFT with their metadata, so knowing the id of an NFT, you can easily find out who owns it and what it represents. The metadata is just text and can be stored directly in the contract or somewhere else on the Internet, in which case the address of the metadata is stored in the contract. -The contract that holds the NFTs can implement different features according to the platform, it can allow its users to transfer their NFTs to other users of the contract, sell them, burn them, track royalties for every purchase, etc. As the smart contract is just a piece of code, the possibilities are virtually limitless! - -### Useful lexicon - -* To mint an NFT: to create an NFT and record its data into a smart contract -* To burn an NFT: to delete the data associated with an NFT from a smart contract -* A smart contract: a piece of autonomous code that lives on a blockchain -* The [IPFS](https://ipfs.io/#how): a network of computers providing decentralized storage -* To pin on the IPFS: storing data on the IPFS - -### Creating an NFT platform on Tezos - -Now comes the time to look at some code πŸ‘€ - -Our simple NFT platform will be made of 3 different parts: - -* The **contract** written in Ligo will securely store the NFT ids and metadata and allow the users to mint, transfer, and burn their tokens -* The **backend** app written in Express \(JavaScript\) will provide us with a secure way of pinning the metadata to the IPFS and ensure that they are not tampered with -* The **frontend** app written in Svelte \(JavaScript\) will provide a user-friendly interface to interact with the contract and the backend. - -These three parts of the platform will communicate with each other at some point: the frontend talks to the contract when a user starts the minting process of a new NFT and to the backend to pin the metadata and the picture on the IPFS. The backend talks to the frontend to provide the IPFS hash \(also called a [**CID**](https://docs.ipfs.io/concepts/content-addressing/)\) before minting the NFT. The contract just listens because Michelson contracts do not return any value, they don’t talk πŸ™‚ - -#### The contract - -The goal of this tutorial is not to create an FA2 contract from scratch but rather to understand the principles of such a contract. You can find amazing templates of FA2 contracts in the [TQ Tezos repository on Github](https://github.com/tqtezos/smart-contracts). This app uses a modified version of their NFT contract. - -An FA2 contract generally consists of the following parts: - -* A bigmap called **ledger** whose purpose is to associate the token ids created in the contract with their owner -* A bigmap called **metadata** records the metadata associated with the contract itself \(its name, version, etc.\) -* A bigmap called **token\_metadata** records the metadata associated with every token stored in the contract -* An entrypoint called **transfer** allows \(or forbids\) the transfer of one or multiple tokens from one address to another -* An entrypoint called **update\_operators** allows owners of tokens to give permission to other addresses to handle their tokens. This can be useful, for example, if the contract implements a marketplace where you can set your NFTs on sale and let the contract handle the sale -* An entrypoint often called **mint** \(or its variations\) creates new tokens with the provided data. This is where the token metadata is provided to be stored and where the token id is assigned to the NFT that’s being created. - -> Note: in addition to these entrypoints and bigmaps, an NFT contract can implement other structures according to its use case, for example, you can have a bigmap with all the NFTs on sale and different entrypoints to set an NFT on sale, to purchase it or to withdraw it from the marketplace, you can have a burn entrypoint to destroy the NFTs you don’t want on the platform anymore, etc. - -> It is essential to understand the difference between β€œ**metadata”** and β€œtoken\_metadata”. The β€œmetadata” bigmap holds information about the contract itself while the β€œtoken\_metadata” bigmap holds information about every single token stored in the contract. - -The contract we will use for this tutorial is a basic FA2 contract that implements the structures and entrypoints described above. The users of the platform will be able to mint, transfer and burn their NFTs. - -You can have a look at the contract [at this address](https://github.com/claudebarde/taquito-pinata-tezos-nft/blob/main/contract/NFTS_contract.mligo). - -#### The backend - -The backend of the app is a simple Express app written in TypeScript. The app only exposes a single route, β€œ`/mint`”, that will be called to create the NFT metadata and pin it on the IPFS with the associated picture. Before continuing with the code, you must set up an account with [Pinata](https://pinata.cloud/) and get your API keys. - -First step, sign up to create an account and follow the instructions: - -![](/images/nft-pinata/image36.png) - -When you are all set up, click on β€œAPI Keys” in the left panel: - -![](/images/nft-pinata/image22.png) - -To finish, click on β€œ_+ New Key_” to get your keys: - -![](/images/nft-pinata/image9.png) - -You will get an API key and a secret key, copy-paste them somewhere safe to use them later as they won’t be visible anymore after that. - -The app uses 5 packages: - -![](/images/nft-pinata/image34.png) - -* **express** allows us to set up a server app quickly -* **@pinata/sdk** gives us convenient functions to interact with our Pinata account -* **fs** \(or file system\) is a package already installed in Node JS that we will use to manipulate the picture sent by the user -* **cors** allows us to set up a CORS policy for our app and avoid unwanted requests from unauthorized sources -* **multer** is a package that will make handling the picture sent by the user a lot easier - -Next, we have to do some setup before writing the β€œmint” endpoint. Because I used [Heroku](https://id.heroku.com/login) to host the app, there is also some Heroku-specific setting up to do to start the server: - -![](/images/nft-pinata/image28.png) - -Heroku doesn’t like it too much when you try to tell it which port to use πŸ˜… So for the production version, you must let Heroku decide on which port your app is going to listen to. - -Setting up the Pinata SDK will also depend on the `process.env.NODE_ENV` variable. You can choose to have your API keys in a separate file, both in the development and production environment, but Heroku lets you define environment variables that are automatically injected in your build and stored securely, so this is generally the solution you would prefer, i.e having a separate file with your keys for development and having your keys in environment variables for production. Whichever solution you choose, the Pinata SDK can be easily instantiated by passing the API key and the secret key as parameters: - -![](/images/nft-pinata/image20.png) - -Let’s finish setting up the server app: - -![](/images/nft-pinata/image29.png) - -In the `corsOptions` variable, we indicate the URLs that are allowed to communicate with the server. During development, you should allow `localhost` with the port you are using, then you can use the URL of your app. - -Now, we can set up the different middlewares: - -* `upload` is a middleware returned by `multer` that we set by passing an object whose `dest` property is the path to the folder where we want to store the picture we will receive -* `cors` with the options set up above -* `express.json({ limit: β€œ50mb” })` allows the app to receive up to 50 MB of JSON \(which will be necessary to pass the picture\) -* `express.urlencoded({ limit: β€œ50mb”, extended: true, parameterLimit: 50000 })` works in conjunction with the setting above and allows the server to receive a picture up to 50 MB in size - -Now, everything is set up, let’s write the `mint` endpoint! - -![](/images/nft-pinata/image32.png) - -This is going to be a `POST` endpoint \(because of the picture we need to receive\) that’s going to be called when a request comes to the `/mint` route. We use the `single` method of the `upload` middleware from `multer` with the `β€œimage”` parameter, which tells `multer` that we are expecting to receive one image on this endpoint. We then store the request in a new variable cast to the `any` type because TypeScript will raise an error later as it is unaware that the request has been modified by `multer`. - -The request comes with the file sent by the user: - -![](/images/nft-pinata/image14.png) - -We check first if a file was provided with `if(!multerReq.file)`, if there is none, the request fails with a 500 error code and a message. If a file was provided, we store the filename available at `multerReq.file.filename`. - -After checking if the request came along with a file, we’re going to verify that our connection to the Pinata service works properly: - -![](/images/nft-pinata/image21.png) - -The instance of the Pinata SDK provides a method called `testAuthentication` that verifies that you are properly authenticated. With that done, we can go ahead and pin the user’s picture in Pinata: - -![](/images/nft-pinata/image24.png) - -> Note: we have to pin the picture first before pinning the metadata to the IPFS because the metadata must include the hash of the picture. - -To pin a file to the IPFS using the Pinata SDK, you must provide a [readable stream](https://nodejs.org/api/stream.html). This can be easily achieved by using the `createReadStream` method of the `fs` package that you call with the path of the file that you want to convert to a readable stream. Remember that `multer` automatically saved the image in the request in the `uploads` folder, so this is where we will be looking for it. - -After that, we must set some options to pass with the file, mainly so we can identify the file easily among the other files we pinned in our Pinata account. The `name` and `keyvalues` of the `pinataMetadata` property can be anything you want, the `name` property is going to be displayed in the pin manager of the Pinata website. - -Next, we can pin the picture to the IPFS. We use the `pinFileToIPFS` method of the Pinata SDK and pass as arguments the readable stream we created earlier and the options. This returns a promise that resolves with an object containing 2 properties we verify to make sure the pinning was successful: the `IpfsHash` property holds the IPFS hash of the file we’ve just pinned and the `PinSize` property holds the size of the file. If these 2 properties are defined and not equal to zero, we can assume the file was correctly pinned. - -Now, we can create the metadata for the NFT and pin it to the IPFS: - -![](/images/nft-pinata/image41.png) - -First, we are going to remove the user’s image from the server. Whether you are using a service on a free tier with a limited storage or you have your own server, you don’t want to keep the images the users sent on your server. To remove it, you can use the `unlinkSync` method of the `fs` package and pass to it the path to the file. - -The metadata must follow a certain structure to help the dapps in the Tezos ecosystem read their properties correctly. Here are a few of the properties you can set: - -* `name` => the name of the NFT -* `description` => a description of the NFT -* `symbol` => the symbol will appear in wallets to represent your NFT, choose it wisely -* `artifactUri` => the link to the asset formatted as `ipfs://` + the IPFS hash -* `displayUri` => the link to the picture formatted as `ipfs://` + the IPFS hash -* `creators` => a list of the creators of the NFT -* `decimals` => decimals are always set to `0` for NFTs -* `thumbnailUri` => the thumbnail to display for the NFT \(for example, in wallets\) -* `is_transferable` => whether the NFT can be transferred or not -* `shouldPreferSymbol` => allows wallets to decide whether or not a symbol should be displayed in place of a name - -Once we created the object that will become the metadata of the NFT, we can pin it to the IPFS. The Pinata SDK offers a `pinJSONToIPFS` method to do what it says, pin JSON to the IPFS πŸ˜… You can pass to it your JavaScript object directly \(I assume the SDK converts it into JSON because passing a JSON string throws an error\) and just like with the picture, you can set some metadata for the metadata! Once the promise resolves, we check if we got the IPFS hash back and that the data size is over 0. Now everything is pinned! We can send a simple response and attach the CID for the metadata and for the picture: - -``` sh -res.status(200).json({ - status: true, - msg: { - imageHash: pinnedFile.IpfsHash, - metadataHash: pinnedMetadata.IpfsHash - } -}); -``` - -The two hashes will confirm on the frontend side that the picture and the metadata have been correctly pinned. - -#### The frontend - -The app we will build for the frontend has the typical structure of a Tezos app so we will only focus on the functions required to get the picture and the metadata from the user and send them to the backend before minting the NFT and to display the NFTs the user may own. If you are interested in learning how to build a Tezos app, you can follow [this tutorial](https://medium.com/ecad-labs-inc/how-to-build-your-first-tezos-dapp-2021-edition-b1263b4ba016) to learn everything you need to know! - -_1- Displaying the NFTs_ - -As explained earlier, the NFTs are just token ids stored in the contract. In order to find the NFTs owned by the users connected to the dapp, we just have to find the token ids associated with their addresses. The contract for this tutorial implements a convenient **reverse ledger** that allows you to fetch all the token ids associated with an address in a single call. - -> Note: a reverse ledger is not a standard feature of NFT contracts and it may be absent from other platforms. If that’s the case, they may implement other ways of tracking token ids owned by a wallet address, for example, an external ledger file. - -Let’s start by installing [Taquito](https://tezostaquito.io/) and creating a new instance of the Tezos toolkit: - -![](/images/nft-pinata/image8.png) - -Now, we can fetch the storage of the contract: - -![](/images/nft-pinata/image10.png) - -`await Tezos.wallet.at(contractAddress)` creates an instance of the contract with different useful methods to interact with the contract or get details about, like the storage, that you can get using `await contract.storage()`. After that, we have access to the whole storage. - -Now, we can look for the token ids owned by the user by searching the `reverse_ledger` bigmap with the `get` function: - -![](/images/nft-pinata/image19.png) - -`getTokenIds` is an array containing all the ids owned by the `address`. We can simply loop through the array to get each id and look for the id in the `ledger` bigmap: - -![](/images/nft-pinata/image37.png) - -The id is returned by Taquito as a `BigNumber`, so you have to call `.toNumber()` first before being able to use it. Once we have the id, we can look for its metadata in the `token_metadata` bigmap. The value returned is a Michelson map and the metadata path is going to be stored at the empty key. Because the path is stored as bytes, we use `bytes2Char()` provided by the `@taquito/utils` package to convert the returned `bytes` into a `string`. To finish, we return an object with 2 properties: the token id and the IPFS hash of the metadata. - -> Note: although the standard requires us to store the IPFS hash in the following manner => `ipfs://IPFS_HASH`, there is no safeguard and any kind of data can be stored there, this is why we make a simple check with `tokenInfo.slice(0, 7) === β€œipfs://”` using the ternary operator to verify that at least this condition is fulfilled. - -_2- Sending the picture and metadata to the backend_ - -First, we set up the HTML tags we need to get the picture, the name of the picture, and its description: - -![](/images/nft-pinata/image23.png) - -The `bind` attribute in Svelte makes it very easy to store the input in a variable that we can use later when we want to pin the NFT to the IPFS. A click on the `upload` button will trigger the upload of the picture, its title, and description to the server. - -Now, let’s see how uploading the user data works! - -![](/images/nft-pinata/image26.png) - -We define 2 boolean variables called `pinningMetadata` and `mintingToken` that we will update according to the result of the different steps of the upload to give some visual feedback to the users in the UI. Because we are not using a traditional form, we must build the form data manually. After instantiating a new `FormData`, we use the `append` method to add the different details of the form, the picture, the title, the description, and the creator of the NFT. - -Once the form is ready, we can use the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to make a POST request to the `/mint` endpoint of our server app. The request should include the required headers and the form in the `body`. The response from the server will include the hash for the picture and the hash for the metadata: - -![](/images/nft-pinata/image33.png) - -When the `response` comes, we can convert it to a usable JS object with the `json` method. We check that the `status` property is `200` and that the `metadataHash` and `imageHash` properties exist. If that’s the case, we can switch the UI from β€œpinning” to β€œminting” and send the transaction to the blockchain to save the NFT metadata: - -![](/images/nft-pinata/image15.png) - -This is a regular contract call. You create an instance of the contract by calling `Tezos.wallet.at(contractAddress)`, then you call the `mint` entrypoint in the `contract.methods` property. Because the entrypoint expects bytes, we have to convert the IPFS hash into bytes without forgetting to prefix `ipfs://` to make it valid. We pass the `userAddress` at the same time to identify the owner of the NFT in the contract. After the NFT is minted and the minting is confirmed, we save the data of the NFT into `newNft` to have it displayed in the interface, we reset the files, title, and description variables to give the opportunity to the user to mint another NFT and we refresh the list of NFTs owned by the user by querying them \(this is not absolutely necessary but getting up-to-date data from the contract never hurts\). - -Now, the NFT has been successfully minted, its metadata is pinned on the IPFS and it is available to the world πŸ₯³ - -### Suggested improvements - -The purpose of this tutorial is to build a simple NFT platform and introduce some concepts related to creating and minting NFTs, in general, and specifically on the Tezos blockchain. Here are a few additional features and design considerations you would like to take into account for a fully-featured NFT app: - -* Generate the IPFS hashes client-side first before pinning them: a failed transaction and other worst-case scenarios may leave unused content pinned into your Pinata account, to avoid this, you can spin up an IPFS node in the client browser, pin the data, mint the NFT and then pin it to your Pinata account -* Add a `burn` endpoint: right now, your users can only create tokens, but you could also allow them to delete their NFTs -* Display other NFTs of the platform in the front-end interface -* Add a fee to mint new NFTs: when sending a call to the mint entrypoint, add `.send({ amount: fee })` to monetize your service. - -If you want to get your hands dirty, you can also improve the contract. You can add a marketplace to the contract where NFT creators can sell their artwork, you can implement royalties every time an NFT is sold, you can track the sales and their amount and create a β€œreputation” system for the artists, etc., the possibilities are endless! - -### Conclusion - -This tutorial introduced a lot of information about NFTs. You learned about the 3 different parts that make up an NFT platform: the contract that records the NFT ids and a link to their associated metadata, the backend that securely builds the metadata and pins it to the IPFS, and the frontend that collects the picture and the related information from the user before minting the NFT. These 3 elements work in concert to receive the user’s input, process it, format it, save it on the IPFS, and record it on the Tezos blockchain. - -These 3 parts of the minting and pinning process require 3 tools that are the cornerstones of building NFT platforms on Tezos: a smart contract language like [Ligo](https://ligolang.org/) to write the smart contract, an IPFS pinning service like [Pinata](https://pinata.cloud/) to easily save data to the IPFS, and a JavaScript library like [Taquito](https://tezostaquito.io/) to let the users interact with the smart contract. This is everything you need to build yourself the next Hic et Nunc! - diff --git a/src/pages/tutorials/create-an-nft/nft-taquito/index.md b/src/pages/tutorials/create-an-nft/nft-taquito/index.md new file mode 100644 index 000000000..b2f2e841d --- /dev/null +++ b/src/pages/tutorials/create-an-nft/nft-taquito/index.md @@ -0,0 +1,704 @@ +--- +id: nft-taquito +title: Create a web app that mints NFTs +authors: 'Sol Lederer, Tim McMackin' +lastUpdated: 20th September 2023 +--- + +This tutorial covers how to set up a decentralized web application (dApp) that allows users to create NFTs on Tezos. +No prior knowledge of NFTs or Tezos is required, but because the tutorial application uses TypeScript, some familiarity with JavaScript or TypeScript makes it easier to understand. + +In this tutorial, you will learn: + +- What NFTs are +- How to set up distributed storage for NFT metadata +- How to deploy (originate) a smart contract to Tezos +- How to use the [Taquito](https://tezostaquito.io/) JavaScript/TypeScript SDK to access Tezos and user wallets and to send transactions to Tezos + +## What is a non-fungible token (NFT)? + +An NFT is a special type of blockchain token that represents something unique. +Fungible tokens such as XTZ and real-world currencies like dollars and euros are interchangeable; each one is the same as every other. +By contrast, each NFT is unique and not interchangeable. +NFTs can represent ownership over digital or physical assets like virtual collectibles or unique artwork, or anything that you want them to represent. + +Like other types of Tezos tokens, a collection of NFTs is managed by a smart contract. +The smart contract defines what information is in each token and how the tokens behave, such as what happens when a user transfers an NFT to another user. +It also keeps a ledger that records which account owns each NFT. + +In this tutorial, you create NFTs that comply with the FA2 standard (formally known as the [TZIP-12](https://gitlab.com/tezos/tzip/-/blob/master/proposals/tzip-12/tzip-12.md) standard), the current standard for tokens on Tezos. +The FA2 standard creates a framework for how tokens behave on Tezos, including fungible, non-fungible, and other types of tokens. +It provides a standard API to transfer tokens, check token balances, manage operators (addresses that are permitted to transfer tokens on behalf of the token owner), and manage token metadata. + +## Tutorial application + +The application that you set up in this tutorial has three parts: + +- The **smart contract** runs on the Tezos blockchain to manage the NFTs, including creating, transferring, and destroying them +- The **backend application** runs on a web server to upload the NFT data to the internet +- The **frontend application** runs on a web server and allows the user to connect their wallet, enter the information for the NFT, and send a request to the smart contract to create the NFT + +This diagram shows what happens when the user creates an NFT with the application. +The rest of this tutorial covers these steps in detail: + +![Flow diagram of the tutorial application and interaction between the user and the parts of the application](/images/nft-create/taquito-application-flow.png) + +The frontend application looks like this, with fields for the image to represent the NFT and for its metadata: + +![The home page of the frontend application](/images/nft-create/taquito-application-home.png) + +## Prerequisites + +To run this tutorial you need Node.JS and NPM installed. +See . +You can verify that they are installed by running these commands: + + ```bash + node --version + npm --version + ``` + +## Configure IPFS storage + +NFTs have metadata, usually including at least a title and description. +Optionally, the NFT can include many other metadata fields, such as links to media and attributes in many different formats. + +Because storage space on blockchains is expensive, developers don't put entire token metadata files on Tezos. +Instead, they configure decentralized storage for the NFT data and put only the link to that data on Tezos itself. +In this section, you set up storage for the NFT metadata using the InterPlanetary File System (IPFS) protocol. + +IPFS requires authentication just like blockchain transactions, so in this section you set up an account with the Pinata IPFS provider and use it to upload (or _pin_) the NFT data to IPFS. + +1. Create a free Pinata account at . + +1. Go to the API Keys tab and click **New Key**. + +1. On the Create New API Key page, expand **API Endpoint Access > Pinning** and enable the `pinFileToIPFS` and `pinJSONToIPFS` permissions, as in this picture: + + ![Selecting the permissions for the Pinata key](/images/nft-create/pinata-key-permissions.png) + +1. In the **Key Name** field, give the key a name, such as "My Key." + +1. Click **Create Key**. + + The API Key Info window shows the API key and secret, which you must copy immediately, because they are not shown again. + +1. Copy the API Key and API Secret fields and save the values on your computer. +You need these values in the next section. + + You can see the new API key on the API Keys tab: + + ![The new Pinata API key in the Pinata web app](/images/nft-create/created-pinata-key.png) + +Now your applications can use your Pinata account to pin NFT data to IPFS. + +## Download the tutorial files + +The tutorial application has three parts: + +- The smart contract that manages the NFTs +- The backend application that handles uploading data to IPFS +- The frontend application that connects to the user's wallet, sends the data to the backend application, and sends the transactions to the smart contract to mint the NFTs + +The tutorial application files are in this GiHub repository: . + +If you have the `git` program installed, you can clone the repository with this command: + +```bash +git clone https://github.com/trilitech/tutorial-applications.git +``` + +If you don't have git installed, go to and click "Code > Download ZIP" and extra the ZIP file on your computer. + +Then, go to the application in the `nft-taquito` folder. + +## The tutorial contract + +The file `contract/NFTS_contract.mligo` contains the code for the smart contract that manages the NFTs. +This contract is written in the CameLIGO version of the LIGO smart contract programming language, with a syntax similar to OCaml. +This contract is already written for you, so do not need any experience with these languages to run the tutorial. + +This contract creates NFTs that comply with the FA2 standard (formally known as the [TZIP-12](https://gitlab.com/tezos/tzip/-/blob/master/proposals/tzip-12/tzip-12.md) standard), the current standard for tokens on Tezos. +The FA2 standard creates a framework for how tokens behave on Tezos, including fungible, non-fungible, and other types of tokens. +It provides a standard API to transfer tokens, check token balances, manage operators (addresses that are permitted to transfer tokens on behalf of the token owner), and manage token metadata. + +The full details of the smart contract are beyond the scope of this tutorial, but the major parts of the contract have descriptions in comments. +For more examples of smart contracts, see [oxheadalpha/smart-contracts](https://github.com/oxheadalpha/smart-contracts) on GitHub. + +### Contract entrypoints + +Like APIs, smart contracts have _entrypoints_, which are commands that transaction senders (client applications such as your web app or other contracts) can call. +To comply with the TZIP-12 standard, the smart contract must have these entrypoints: + +- `transfer`: Transfers tokens from one account to another +- `balance_of`: Provides information about the tokens that an account owns +- `update_operators`: Changes the accounts that can transfer tokens + +This contract includes these additional entrypoints: + +- `mint`: Creates NFTs +- `burn`: Destroys NFTs + +### Contract types + +Because Tezos uses strongly-typed languages, this contract's code starts by defining the types that the contract uses. +These types are important for verifying that data is in the correct format, such as the parameters the transaction senders pass. + +For example, the `transfer` entrypoint accepts a list of the `transfer` type. +This type includes the account to transfer tokens from and a list of the `transfer_destination` type, which includes the account to transfer tokens to, the ID of the token to transfer, and the amount to transfer: + +```ocaml +type token_id = nat + +type transfer_destination = +[@layout:comb] +{ + to_ : address; + token_id : token_id; + amount : nat; +} + +type transfer = +[@layout:comb] +{ + from_ : address; + txs : transfer_destination list; +} +``` + +The type `fa2_entry_points` is a special type that the contract's `main` function uses to define the entrypoints. +It maps the entry points to the type of parameter that they accept: + +```ocaml +type fa2_entry_points = + | Transfer of transfer list + | Balance_of of balance_of_param + | Update_operators of update_operator list + | Mint of mint_params + | Burn of token_id +``` + +### Error messages + +The contract defines a series of error messages, and comments in the code describe what each error message means. +For example, the `balance_of` and `transfer` entrypoints return this error if the client requests information about a token that does not exist or tries to transfer a token that does not exist: + +```ocaml +(** One of the specified `token_id`s is not defined within the FA2 contract *) +let fa2_token_undefined = "FA2_TOKEN_UNDEFINED" +``` + +### Internal functions + +The contract has many internal functions, such as this function, which gets the specified account's balance of tokens. +In the case of NFTs, only one of each token exists, so the function returns a balance of 1 if the account owns the token and 0 if it does not. + +```ocaml +(** +Retrieve the balances for the specified tokens and owners +@return callback operation +*) +let get_balance (p, ledger : balance_of_param * ledger) : operation = + let to_balance = fun (r : balance_of_request) -> + let owner = Big_map.find_opt r.token_id ledger in + match owner with + | None -> (failwith fa2_token_undefined : balance_of_response) + | Some o -> + let bal = if o = r.owner then 1n else 0n in + { request = r; balance = bal; } + in + let responses = List.map to_balance p.requests in + Tezos.transaction responses 0mutez p.callback +``` + +### Main function + +The `main` function is a special function that defines the entrypoints in the contract. +In this case, it accepts the entrypoint that the transaction sender called and the current state of the contract's storage. +Then the function branches based on the entrypoint. + +For example, if the sender calls the `balance_of` entrypoint, the function calls the `get_balance` function and passes the parameters that the sender passed and the current state of the contract's ledger: + +```ocaml + | Balance_of p -> + let op = get_balance (p, storage.ledger) in + [op], storage +``` + +Here is the complete code of the `main` function: + +```ocaml +let main (param, storage : fa2_entry_points * nft_token_storage) + : (operation list) * nft_token_storage = + match param with + | Transfer txs -> + let (new_ledger, new_reverse_ledger) = transfer + (txs, default_operator_validator, storage.operators, storage.ledger, storage.reverse_ledger) in + let new_storage = { storage with ledger = new_ledger; reverse_ledger = new_reverse_ledger } in + ([] : operation list), new_storage + + | Balance_of p -> + let op = get_balance (p, storage.ledger) in + [op], storage + + | Update_operators updates -> + let new_ops = fa2_update_operators (updates, storage.operators) in + let new_storage = { storage with operators = new_ops; } in + ([] : operation list), new_storage + + | Mint p -> + ([]: operation list), mint (p, storage) + + | Burn p -> + ([]: operation list), burn (p, storage) +``` + +### Initial storage state + +An FA2 contract usually contains these properties in its storage: + +- A table named `ledger` that records the token IDs and the address of the current owner +- A table named `metadata` that records the metadata associated with the contract itself, such as its name, creator, and what standards it meets +- A table named `token_metadata` that records the metadata associated with every token stored in the contract + +In this case, the contract uses the CameLIGO `bigmap` type for these tables. + +This contract's storage includes other properties, including the list of operators and the next token ID. + +When you originate (deploy) the contract to Tezos, you must set the initial state of its storage. +For this contract, the initial storage state is in the comment at the end of the file. + +## Originate (deploy) the smart contract to the testnet + +There are many ways to originate a contract on Tezos. +For a tutorial on using the command line, see [Deploy a smart contract](../../deploy-your-first-smart-contract/). + +Before you originate your contract to the main Tezos network (referred to as *mainnet*), you can originate it to a testnet. +Testnets are useful for testing Tezos operations because testnets provide tokens for free so you can work with Tezos without spending real tokens. + +This tutorial uses the online LIGO IDE at because you don't have to install any tools to use it. + +Follow these steps to originate the smart contract to Tezos: + +1. In a web browser, open the IDE at . + +1. At the top right, click the **Network** drop-down list and next to **Tezos**, select the **Ghostnet** testnet. +The network changes to the Ghostnet testnet, as in this picture: + + ![The IDE menu, showing the Ghostnet testnet selected](/images/nft-create/web-ligo-ide-ghostnet.png) + +1. Create an account to use to originate the contract: + + 1. At the top right, click the **Keypair Manager** button. + The "Keypair Manager" window opens. + + 1. Click **Create**. + + 1. Give the keypair a name such as "My keys" and click **Create**. + + 1. From the "Keypair Manager" window, copy the address of the new account, which begins with `kt1`. + You will need this address later. + + 1. Click **Close**. + +1. Send funds to the account from the testnet faucet: + + 1. Go to the Ghostnet faucet at . + + 1. Put the new account address in the **Or fund any address** field. + + 1. Click the button to request 100 tokens and wait for the browser to complete the operation. + + 1. When you see a message that the tokens are sent to your address, go back to the web IDE, open the "Keypair Manager" window and verify that the account has tokens, as in this example: + + ![The IDE Keypair Manager window, showing an account with funds](/images/nft-create/web-ligo-ide-account.png) + +1. In the IDE, Click the **New** button. + +1. In the "Create a New Project" window, give your project a name, such as "NFT tutorial," select "Empty Project" in the **Template** field, and select "CameLIGO" in the **Syntax** field. + +1. Click **Create Project**. +The IDE opens a blank contract file and shows commands for the file on the left-hand side of the window. + +1. Paste the contents of the `contract/NFTS_contract.mligo` file into the editor. +The IDE saves the file automatically. + +1. Click **Compile** and then in the "Compile" window, click **Compile**. +The IDE compiles the contract code to Michelson, the base language that all Tezos contracts use. +At the bottom of the window, it prints the message `wrote output to .workspaces/NFT tutorial/build/contracts/Contract.tz`. +If you see an error, make sure that you copied the entire contract file. + +1. Originate the contract: + + 1. Click **Deploy**. + + 1. In the "Deploy contract" window, in the **Init storage** field, paste the initial storage value for the contract, which you can get from the comment at the end of the contract: + + ```ocaml + { + ledger = (Big_map.empty: (token_id, address) big_map); + operators = (Big_map.empty: ((address * (address * token_id)), unit) big_map); + reverse_ledger = (Big_map.empty: (address, token_id list) big_map); + metadata = Big_map.literal [ + ("", Bytes.pack("tezos-storage:contents")); + ("contents", ("7b2276657273696f6e223a2276312e302e30222c226e616d65223a2254555473222c22617574686f7273223a5b2240636c617564656261726465225d2c22696e7465726661636573223a5b22545a49502d303132222c22545a49502d303136225d7d": bytes)) + ]; + token_metadata = (Big_map.empty: (token_id, token_metadata) big_map); + next_token_id = 0n; + admin = ("tz1Me1MGhK7taay748h4gPnX2cXvbgL6xsYL": address); + } + ``` + + 1. In the **Signer** field, make sure your new account is selected. + + 1. Leave the other fields blank and click **Estimate**. + The IDE calculates the fees for the deployment. + + 1. Click **Deploy** and leave the window open to wait for the contract to be originated. + + Originating the contract can take a few minutes. + When the transaction completes, the window shows a message that the contract was deployed. + + 1. Copy the contract address, which starts with `KT1`, and then close the "Deploy contract" window. + +1. Verify that the contract is on the testnet by finding it on a block explorer: + + 1. Open a Tezos block explorer such as [TzKT](https://tzkt.io) or [Better Call Dev](https://better-call.dev/). + + 1. Set the explorer to Ghostnet instead of mainnet. + + 1. Paste the contract address into the search field and press Enter. + + 1. Go to the Entrypoints tab to see the entrypoints and their parameters. + +Now anyone can call the Tezos contract if they have tokens for the fees and send a valid request. + +## Run the backend application + +The backend application is responsible for uploading the NFT data to IPFS. +In these steps, you configure the backend application with your Pinata information: + +1. In your command-line window, go to the `backend` folder of the tutorial application. + +1. Install its dependencies by running `npm install`. + +1. In the `src/PinataKeys.ts` file, replace `DUMMY_KEY` with your Pinata API key and replace `DUMMY_SECRET` with your Pinata secret. + +1. Open the `src/index.ts` file. + + The `corsOptions` constant in this file contains the location of the frontend application. + It includes the default location of the frontend application (`http://localhost:5173`) and an example custom URL. + + ```typescript + const corsOptions = { + origin: ["http://localhost:5173", "https://my-cool-nft-app.com"], + optionsSuccessStatus: 200 // some legacy browsers (IE11, various SmartTVs) choke on 204 + }; + ``` + +1. If you intend to host the frontend application anywhere other than on your computer, add its URL to the `corsOptions` constant. + + If you are not going to change where the frontend application is hosted, you can leave this code unchanged. + +1. Review the code for the `POST /mint` endpoint. + + This endpoint is the only endpoint that the frontend application uses. + First, it accesses the image file that the user uploads and uses the Pinata `pinFileToIFPS` method to pin it to IPFS with a name and description: + + ```typescript + const readableStreamForFile = fs.createReadStream(`./uploads/${fileName}`); + const options: any = { + pinataMetadata: { + name: req.body.title.replace(/\s/g, "-"), + keyvalues: { + description: req.body.description + } + } + }; + const pinnedFile = await pinata.pinFileToIPFS( + readableStreamForFile, + options + ); + ``` + + The app must pin the picture first because the token metadata includes a hash of the picture. + + Then, it creates a metadata object with information about the NFT, including its name and description and the IPFS URI of the image and pins that metadata object to IPFS: + + ```typescript + if (pinnedFile.IpfsHash && pinnedFile.PinSize > 0) { + // remove file from server + fs.unlinkSync(`./uploads/${fileName}`); + // pins metadata + const metadata = { + name: req.body.title, + description: req.body.description, + symbol: "TUT", + artifactUri: `ipfs://${pinnedFile.IpfsHash}`, + displayUri: `ipfs://${pinnedFile.IpfsHash}`, + creators: [req.body.creator], + decimals: 0, + thumbnailUri: "https://tezostaquito.io/img/favicon.png", + is_transferable: true, + shouldPreferSymbol: false + }; + + const pinnedMetadata = await pinata.pinJSONToIPFS(metadata, { + pinataMetadata: { + name: "TUT-metadata" + } + }); + // ... + } + ``` + + This metadata object includes several standard fields for Tezos tokens, including a name, description, symbol to show in wallets, and URIs to preview pictures and thumbnails. + The `decimals` field is set to 0 because the NFT cannot be divided into decimal amounts like fungible tokens can. + + If both of the pins were successful, the endpoint returns the IPFS URIs of the image and metadata to the frontend application: + + ```typescript + if (pinnedMetadata.IpfsHash && pinnedMetadata.PinSize > 0) { + res.status(200).json({ + status: true, + msg: { + imageHash: pinnedFile.IpfsHash, + metadataHash: pinnedMetadata.IpfsHash + } + }); + } else { + res + .status(500) + .json({ status: false, msg: "metadata were not pinned" }); + } + ``` + +1. Start the backend application by running `npm run dev`. + +## Run the frontend application + +The front application accepts information about a new NFT from the user, sends the image and metadata to the backend, and calls the smart contract to mint the NFT. +Follow these steps to configure and start the frontend application: + +1. In your command-line window, go to the `frontend` folder of the tutorial application. + +1. Install its dependencies by running `npm install`. + +1. Open the `src/App.svelte` file. + +1. Set the `contractAddress` constant to the address of the originated smart contract, which starts with `KT1`: + + ```typescript + const contractAddress = "KT1XdU2tK5hoDhtToP4kSSR9HiCkie4mZqFp"; + ``` + +1. Review the code for the `onMount` function: + + This function runs when the page loads. + It starts by creating an instance of the Taquito `TezosToolkit` object, which provides access to Tezos. + It also creates an object to unpack data from the map properties in the contract's storage: + + ```typescript + Tezos = new TezosToolkit(rpcUrl); + Tezos.setPackerProvider(new MichelCodecPacker()); + ``` + + It creates an instance of the Beacon `BeaconWallet` object to prepare to connect to the user's wallet, but it does not connect to the user's wallet immediately. + It could try to connect to the user's wallet immediately, but it's better programming practice to wait and let the user click a button to connect their wallet after the page loads. + + ```typescript + wallet = new BeaconWallet(walletOptions); + ``` + + The application can remember if it has a connection to a wallet, so if the user has connected their wallet before, it connects to that wallet automatically: + + ```typescript + if (await wallet.client.getActiveAccount()) { + userAddress = await wallet.getPKH(); + Tezos.setWalletProvider(wallet); + await getUserNfts(userAddress); + } + ``` + +1. Review the code for the `connect` function. + + First, this function uses the [Beacon wallet SDK](https://docs.walletbeacon.io/) to prompt the user to connect their wallet if it is not already connected: + + ```typescript + if (!wallet) { + wallet = new BeaconWallet(walletOptions); + } + ``` + + Then, it requests permission to swap to the Ghostnet testnet: + + ```typescript + await wallet.requestPermissions({ + network: { + type: NetworkType.GHOSTNET, + rpcUrl + } + }); + ``` + + Finally, it retrieves the user's current NFTs with the `getUserNfts` function: + + ```typescript + await getUserNfts(userAddress); + ``` + +1. Review the code for the `getUserNfts` function: + + This function receives the user's account address. + Then, it retrieves the current storage for the smart contract, including information about which account owns each NFT: + + ```typescript + const getUserNfts = async (address: string) => { + // finds user's NFTs + const contract = await Tezos.wallet.at(contractAddress); + nftStorage = await contract.storage(); + const getTokenIds = await nftStorage.reverse_ledger.get(address); + // ... + } + ``` + + The `reverse_ledger` property in the contract storage keeps a list of who owns each NFT. + However, this property is not a standard feature of NFT smart contracts. + According to the standard, the contract must have a `ledger` property that maps the ID of each token to the address that owns it. + The `reverse_ledger` property indexes this information in the opposite way, so the code can filter the list of tokens according to a given owner's address. + This property is for the convenience of apps accessing the storage and may not be available on other NFT contracts. + + Now that it has the list of IDs of NFTs that the account owns, it retrieves the metadata for each token from the contract storage: + + ```typescript + if (getTokenIds) { + userNfts = await Promise.all([ + ...getTokenIds.map(async id => { + const tokenId = id.toNumber(); + const metadata = await nftStorage.token_metadata.get(tokenId); + const tokenInfoBytes = metadata.token_info.get(""); + const tokenInfo = bytes2Char(tokenInfoBytes); + return { + tokenId, + ipfsHash: + tokenInfo.slice(0, 7) === "ipfs://" ? tokenInfo.slice(7) : null + }; + }) + ]); + } + ``` + + The frontend uses this `userNfts` variable to show the user's NFTs on the page. + +1. Review the `upload` function: + + This function starts by creating an HTML form data object with the image, title, and description for the NFT and the address of the creator, who will own the new NFT. + It sends this data to the backend's `POST /mint` endpoint: + + ```typescript + const data = new FormData(); + data.append("image", files[0]); + data.append("title", title); + data.append("description", description); + data.append("creator", userAddress); + + const response = await fetch(`${serverUrl}/mint`, { + method: "POST", + headers: { + "Access-Control-Allow-Origin": "*" + }, + body: data + }); + ``` + + It receives the response from the backend with the URIs for the NFT metadata. + Then, it creates a Taquito `ContractAbstraction` object to represent the smart contract and provide access to the contract's methods and storage: + + ```typescript + const contract = await Tezos.wallet.at(contractAddress); + ``` + + It uses this object to call the contract's `mint` entrypoint and pass the metadata and wallet address: + + ```typescript + const contract = await Tezos.wallet.at(contractAddress); + const op = await contract.methods + .mint(char2Bytes("ipfs://" + data.msg.metadataHash), userAddress) + .send(); + console.log("Op hash:", op.opHash); + await op.confirmation(); + ``` + + Finally, it refreshes the list of the user's NFTs with the `getUserNfts` function: + + ```typescript + await getUserNfts(userAddress); + ``` + +1. Start the frontend application by running `npm run dev`. + +## Testing the application + +To test the application, you must have a Tezos wallet and a small amount of XTZ tokens to pay the transaction fees. + +1. Install any wallet that is compatible with Tezos, such as [Temple wallet](https://templewallet.com/) and switch to the Ghostnet network. + +1. Send funds to the wallet from the testnet faucet: + + 1. Go to the Ghostnet faucet at . + + 1. Put the wallet account address in the **Or fund any address** field. + + 1. Click the button to request 100 tokens and wait for the browser to complete the operation. + + 1. When you see a message that the tokens are sent to your address, open your wallet and verify that the tokens are there. + It can take a few minutes for them to appear. + +1. When the frontend application starts, open the web browser to . + +1. Click **Connect your wallet** and approve the connection in your wallet. + +1. Click **Choose file** and select any image for the NFT. + +1. Add a title and description for the new NFT in the **Title** and **Description** fields. + +1. Click **Mint NFT**. + +1. Confirm the transaction in your wallet, which requires a small amount of XTZ for the transaction fees. + +1. Wait for the transaction to complete. +When it completes, the screen changes to a success message with links to the NFT information, as in this picture: + + ![The success message, with links to the NFT information](/images/nft-create/taquito-application-created-nft.png) + + In the "Your NFTs" list, you can click on a number (starting at 0) to see the information that is in the NFT, which includes the name and description that you added and links to data on IPFS. + The page also shows a link to a block explorer where you can see technical information about the minting transaction. + +To see information about the smart contract and NFT collection, go to a block explorer such as , set it to Ghostnet, and enter the address of the smart contract. +The explorer shows information such a list of the tokens in the collection and who owns them. +You can also see the current state of the storage for the contract. + +## Summary + +In this tutorial, you learned how to create an application that mints applications on Tezos on behalf of users. +The parts are independent, so you could code a different frontend application to call the smart contract or use a different backend application to upload the NFT metadata. + +If you want to continue working with this application, try implementing the `burn` entrypoint in the frontend to allow users to destroy their NFTs. +The contract has a `burn` entrypoint, but the frontend does not provide a way to call it. + +You can try adding your own entrypoints and originating a new contract, but you cannot update the existing contract after it is originated. + +You can add a fee to mint NFTs by sending funds along with the mint transaction. +If you add the fee as in the following code, the transaction takes the fee amount along with the transaction fee from the user's wallet. +Currently, the contract does not have a way to send this fee to any other account, so the fee is locked in the contract forever. +You can add an entrypoint to the contract that allows authorized users to withdraw the fee later. + +```typescript +const op = await contract.methods + .mint(char2Bytes("ipfs://" + data.msg.metadataHash), userAddress) + .send({ amount: 1 }); +``` + +You can also optimize the application by generating the IPFS hashes on the client side before pinning them. +If the mint transaction fails, it may leave unused metadata in your IPFS account. +To avoid this problem, you can start an IPFS node in the browser, pin the metadata there, mint the NFT, and pin the metadata to your Pinata account only if the mint transaction succeeds. diff --git a/src/pages/tutorials/create-an-nft/nft-tznft/index.md b/src/pages/tutorials/create-an-nft/nft-tznft/index.md new file mode 100644 index 000000000..82e5d172a --- /dev/null +++ b/src/pages/tutorials/create-an-nft/nft-tznft/index.md @@ -0,0 +1,523 @@ +--- +id: nft-tznft +title: Create an NFT with the `tznft` tool +authors: 'Sol Lederer, Tim McMackin' +lastUpdated: 18th September 2023 +--- + +This tutorial covers how to create a collection of NFTs on Tezos and manipulate them using the `tznft` command-line tool. +No prior knowledge of NFTs or Tezos is required, but you need basic experience with your computer's command-line terminal to paste commands and run them. + +In this tutorial, you will learn: + +- What NFTs are +- How to install and start a local Tezos sandbox environment +- How to create metadata files to describe NFT collections and individual NFTs +- How to create (or _mint_) the NFTs in the sandbox +- How to transfer NFTs and change operator permissions for them +- How to mint NFTs to a testnet + +## What is a non-fungible token (NFT)? + +An NFT is a special type of blockchain token that represents something unique. +Fungible tokens such as XTZ and real-world currencies like dollars and euros are interchangeable; each one is the same as every other. +By contrast, each NFT is unique and not interchangeable. +NFTs can represent ownership over digital or physical assets like virtual collectibles or unique artwork, or anything that you want them to represent. + +Like other types of Tezos tokens, a collection of NFTs is managed by a smart contract. +The smart contract defines what information is in each token and how the tokens behave, such as what happens when a user transfers an NFT to another user. +It also keeps a ledger that records which account owns each NFT. + +In this tutorial, you create NFTs that comply with the FA2 standard (formally known as the [TZIP-12](https://gitlab.com/tezos/tzip/-/blob/master/proposals/tzip-12/tzip-12.md) standard), the current standard for tokens on Tezos. +The FA2 standard creates a framework for how tokens behave on Tezos, including fungible, non-fungible, and other types of tokens. +It provides a standard API to transfer tokens, check token balances, manage operators (addresses that are permitted to transfer tokens on behalf of the token owner), and manage token metadata. + +## Prerequisites + +To run this tutorial you need Node.JS, NPM, and Docker Desktop to install and use the `tznft` CLI tool, which helps you create and test NFT collections on Tezos. + +- Install Node.JS version 18 (not 20) and NPM. +See . +You can verify that they are installed by running these commands: + + ```bash + node --version + npm --version + ``` + + If you see a message with the versions of Node.JS and NPM, they are installed correctly. + +- To install Docker Desktop, see . +Make sure to start Docker Desktop after you install it. + +- To install the `tznft` tool, run this command: + + ```bash + npm install -g @oxheadalpha/tznft + ``` + + You can verify that it is installed by running this command: + + ```bash + tznft --version + ``` + + If you see a message with the version of the `tznft` tool, it is installed correctly. + +## Create a project folder + +1. Create a folder to store your NFT configuration files: + + ```bash + mkdir nft-tutorial + cd nft-tutorial + ``` + +3. Create a starter NFT configuration file: + + ```bash + tznft init + ``` + + The resulting file, named `tznft.config`, contains information about the Tezos networks that are available for you to work with, including the [Ghostnet](https://teztnets.xyz/ghostnet-about) test network and the local sandbox that you set up in the next steps. + The `tznft` tool requires this file, so the commands in the following steps work only from the directory that you ran `tznft init` in. + +4. Check that the default active network is "sandbox:" + + ```bash + tznft show-network + ``` + + The response should show that the active network is "sandbox." + The sandbox is a local simulation of Tezos that you can use to test your work. + +5. Set up a local Tezos sandbox by running this command: + + ```bash + tznft bootstrap + ``` + + This command can take time to run, so wait until you see the message "sandbox started." + + This command uses the [Flextesa](https://tezos.gitlab.io/flextesa/) tool to create a local sandbox in a Docker container. + This sandbox is a local instance of Tezos that you can use to test your work before you send it to a live Tezos network. + The sandbox comes preconfigured with two account aliases named `bob` and `alice` that you can use to test account operations like creating and transferring NFTs. + + You can verify that the sandbox is running by running the command `docker ps` and looking for a container named `flextesa-sandbox`. + To stop the container, run the command `tznft kill-sandbox`, but beware that stopping the container sets the sandbox back to its initial state, which removes any changes you made or contracts or tokens that you created. + + Unlike the live Tezos networks, this sandbox bakes a new block every 5 seconds, by default. + Therefore, commands that you run on the sandbox can take a few seconds to complete. + +## Create NFT metadata + +The first step in creating NFTs is to create local metadata files that describe the collection and the individual NFTs: + +1. Create a collection metadata file by running this command: + + ```bash + tznft create-collection-meta my_collection + ``` + + The new metadata file is named `my_collection.json` and has information such as the name, description, home page, and creator of the collection. + It also includes the interfaces that the NFTs support, including the TZIP-12 interface that was mentioned earlier. + +1. Optional: Edit the `my_collection.json` file to put your information in the `name`, `description`, and `authors` fields. + +1. Validate the collection by running this command: + + ```bash + tznft validate-collection-meta my_collection.json + ``` + + If you did not change values in the file, this command may show warnings that the collection uses placeholder values. + You can continue with these placeholder values or insert your own information. + If there are any errors, make sure that the file is valid JSON. + +1. Create a metadata file for the first NFT in the collection by running this command: + + ```bash + tznft create-nft-meta Token1 bob ipfs://QmRyTc9KbD7ZSkmEf4e7fk6A44RPciW5pM4iyqRGrhbyvj + ``` + + This command creates a metadata file named `Token1.json` with default information about the NFT. + It includes the minter's account address and URIs to pictures that represent the NFT. + In this case, the `ipfs` URI links to a picture of the Tezos logo, which you can see at this link: . + +1. Optional: Edit the metadata such as the name and description fields in the `Token1.json` file. + +1. Optional: Edit other fields in the metadata based on the FA2 standard. + + For example, you can expand the `attributes` section with other attributes. + Each attribute must have the `name` and `value` fields and can optionally have a `type` field, as in this example: + + ```json + "attributes": [ + { + "name": "My string attribute", + "value": "String attribute value" + }, + { + "name": "My integer attribute", + "value": "5", + "type": "integer" + }, + { + "name": "My number attribute", + "value": "12.3", + "type": "number" + }, + { + "name": "My percentage attribute", + "value": "19", + "type": "percentage" + } + ] + ``` + + By default the `artifactUri`, `displayUri`, and `thumbnailUri` fields are set to the picture that you passed in the `tznft create-nft-meta` command. + You can update these to different images to allow applications to show media to represent the NFT. + You can also add a `formats` object to provide media in different formats, such as different image, video, or audio formats: + + ```json + "formats": [ + { + "uri": "ipfs://QmRyTc9KbD7ZSkmEf4e7fk6A44RPciW5pM4iyqRGrhbyvj", + "hash": "a56017a1317b1bc900acdaf600874c00e5c048d30894f452049db6dcef6e4f0d", + "mimeType": "image/svg+xml" + }, + { + "uri": "ipfs://QmRyTc9KbD7ZSkmEf4e7fk6A44RPciW5pM4iyqRGrhbyvj", + "hash": "8968db6bde43255876c464613a31fbd0416ca7d74be4c5ae86c1450418528302", + "mimeType": "image/png", + "dimensions": { + "value": "512x512", + "unit": "px" + } + }, + { + "uri": "ipfs://QmRyTc9KbD7ZSkmEf4e7fk6A44RPciW5pM4iyqRGrhbyvj", + "hash": "d4a93fc8d8991caa9b52c04c5ff7edf5c4bc29317a373e3a97f1398c697d6714", + "mimeType": "model/gltf+json" + } + ] + ``` + + For specifics about what is allowed in an NFT metadata file, see the [TZIP-21](https://gitlab.com/tezos/tzip/-/blob/master/proposals/tzip-21/tzip-21.md) standard. + +1. Validate the NFT metadata file with this command: + + ```bash + tznft validate-nft-meta Token1.json + ``` + + If the file does not validate, verify that it is valid JSON and has only the fields listed in the [TZIP-21](https://gitlab.com/tezos/tzip/-/blob/master/proposals/tzip-21/tzip-21.md) standard. + +1. Create at least one more metadata file for other NFTs by running commands like this example: + + ```bash + tznft create-nft-meta Token2 bob ipfs://QmRyTc9KbD7ZSkmEf4e7fk6A44RPciW5pM4iyqRGrhbyvj + ``` + +## Configure IPFS storage + +Because storage space on blockchains is expensive, developers don't put entire token metadata files on Tezos. +Instead, they configure decentralized storage for the NFT data and put only the link to that data on Tezos itself. +In this section, you set up storage for the NFT metadata using the InterPlanetary File System (IPFS) protocol. + +There are many services that provide access to IPFS, but in this tutorial you use the [Pinata](https://www.pinata.cloud/) IPFS provider. +Pinata requires authentication, so in this section you set up an account with Pinata and use it to upload (or _pin_) the NFT data to IPFS. + +1. Create a free Pinata account at . + +1. Go to the API Keys tab and click **New Key**. + +1. On the Create New API Key page, expand **API Endpoint Access > Pinning,** and enable the `pinFileToIPFS` permission, as in this picture: + + ![Selecting the permissions for the Pinata key](/images/nft-create/pinata-key-permissions.png) + +1. In the **Key Name** field, give the key a name, such as "My Key." + +1. Click **Create Key**. + + The API Key Info window shows the API key and secret, which you must copy immediately, because they are not shown again. + +1. Copy the API Key and API Secret fields and save the values on your computer. +You need these values in the next section. + + You can see the new API key on the API Keys tab: + + ![The new Pinata API key in the Pinata web app](/images/nft-create/created-pinata-key.png) + +1. Add the API key and secret to your local `tznft` configuration by running this command, replacing `$PINATA_KEY` and `$PINATA_SECRET` with your API key and secret: + + ```bash + tznft set-pinata-keys $PINATA_KEY $PINATA_SECRET --force + ``` + + This command stores the key and secret in the `tznft.json` file, so be careful not to share this file. + +1. Pin the first NFT metadata file to IPFS by running this command and passing the file name and a tag for the NFT, which can be the same as the file name: + + ```bash + tznft pin-file Token1.json --tag Token1 + ``` + + The command returns the URI for the data on IPFS, which starts with `ipfs://`. + +1. Copy the IPFS URI, because you will need it later. + +1. In the same way, pin the other NFT metadata files with the `tznft pin-file` command and save their URIs. + +1. Optional: Verify that the files are pinned successfully by opening the Pinata app to the Files page, as in this picture: + + ![The Files tab on Pinata, showing three NFT metadata files](/images/nft-create/pinned-nft-meta.png) + +Now that the metadata is pinned to IPFS, you can create NFTs that link to this metadata. + +## Mint NFTs + +Creating NFTs is called _minting_. +First, you create the smart contract to manage the NFTs. +Then, you mint one or more NFTs with that contract. +The related `tznft` commands use the configuration files that you created earlier. + +1. Create the collection contract from the metadata file by running this command: + + ```bash + tznft create-collection bob --meta_file my_collection.json --alias my_collection + ``` + + This command takes the alias of the user who is the owner of the collection. + In this case, the owner is one of the default accounts in the sandbox. + The command also includes the metadata file and an optional local alias for the collection. + + The command also updates the `tznft.json` file with information about the new collection, including the address of the smart contract that manages the collection. + This smart contract is a pre-compiled FA2 NFT contract written in the [LIGO](https://ligolang.org/) smart contract language. + You can write your own smart contracts to manage NFTs, but using this contract prevents errors and provides all of the functionality needed to create, transfer, and manage NFTs. + +1. Run this command to create a token and set Bob as the owner, replacing the IPFS URI with the URI that the `tznft pin-file` command returned in the previous section: + + ```bash + tznft mint bob my_collection --tokens '1, ipfs://abcde12345' + ``` + + This command includes these parameters: + + - The alias or address of the initial owner. + - The alias of the collection from the `tznft create-collection` command. + - The ID number and IPFS URI for the NFTs in a comma-delimited string. + + If you forgot the IPFS URI, you can look it up in the Pinata app on the Files tab. + This tab has a column labeled "Content Identifier (CID)." + To create the IPFS URI, add the content identifier to the string `ipfs://`. + + The response in the terminal says that the token was minted. + +1. Run the `tznft mint` command to mint the other NFTs. +You can create more than one NFT in a single command by providing more than one string after the `--tokens` switch, as in this example: + + ```bash + tznft mint bob my_collection --tokens '2, ipfs://defgh12345' '3, ipfs://ijklm12345' + ``` + +1. Verify that the NFTs were minted successfully by getting their metadata with the `tznft show-meta` command: + + ```bash + tznft show-meta bob --nft my_collection --tokens 1 2 + ``` + + If the NFTs were created successfully, the command prints the metadata that you pinned to IPFS. + +Now the NFTs are minted to the sandbox. +Because these NFTs are only on your local computer, in the Flextesa sandbox, you can interact with them only locally. +They exist as long as you keep the Flextesa Docker container running, which you started with the `tznft bootstrap` command. + +## Transferring and manipulating NFTs + +The `tznft` command provides commands to manipulate NFTs locally, including transferring them between accounts. +Just like transactions on live blockchain networks, the transaction signer must have permission to transfer or manipulate the NFTs. +Currently, only Bob has access to the NFTs, so the `tznft` commands include him as the signer of most transactions. + +1. Use the `tznft show-balance` command to print information about Bob's NFTs. +This command takes the alias or address of the collection, the signer of the transaction, the owner of the NFTs, and the IDs of one or more NFTs. + + ```bash + tznft show-balance --nft my_collection --signer bob --owner bob --tokens 1 2 + ``` + + Because NFTs are unique, the response shows a balance of 1 if the account owns the token and 0 if it does not, as in this picture: + + ![THe results of the `show-balance` command, with two NFTs in Bob's account](/images/nft-create/show-balance-bob.png) + +1. Use the `tznft show-balance` command to print information about Alice's NFTs: + + ```bash + tznft show-balance --nft my_collection --signer alice --owner alice --tokens 1 2 + ``` + + Because Bob is the initial owner of all of the NFTs, Alice's balance is 0 for each NFT. + +1. Use the `tznft transfer` command to transfer one or more NFTs from Bob to Alice. +This command takes the alias or address of the collection, the signer, and one or more comma-separated strings with the current owner, the new owner, and the ID of the NFT to transfer. +For example, this command transfers NFTs 1 and 2 from Bob to Alice: + + ```bash + tznft transfer --nft my_collection --signer bob --batch 'bob, alice, 1' 'bob, alice, 2' + ``` + +1. Verify that the transfer worked by checking Alice's balance with the `tznft show-balance` command: + + ```bash + tznft show-balance --nft my_collection --signer alice --owner alice --tokens 1 2 + ``` + + Now Alice's balance is 1 for each token that transferred. + Alice is in control of these NFTs and Bob can no longer transfer them. + +1. Verify that Bob does not have control over the transferred NFTs by trying to transfer them back from Alice's account to Bob's account: + + ```bash + tznft transfer --nft my_collection --signer bob --batch 'alice, bob, 1' 'alice, bob, 2' + ``` + + The response shows the error "FA2_NOT_OPERATOR" because Bob's account is not in control of these NFTs. + + You can give Bob's account control over the NFTs by making his account an operator of those NFTs. + +1. Make Bob an operator of Alice's NFTs by passing the token IDs to the `tznft update-ops` command: + + ```bash + tznft update-ops alice --nft my_collection --add 'bob, 1' 'bob, 2' + ``` + +1. Try again to transfer the NFTs from Alice's account to Bob's account with a transaction signed by Bob: + + ```bash + tznft transfer --nft my_collection --signer bob --batch 'alice, bob, 1' 'alice, bob, 2' + ``` + +1. Check Bob's account to see that he now owns the NFTs: + + ```bash + tznft show-balance --nft my_collection --signer bob --owner bob --tokens 1 2 + ``` + +## Freeze the collection + +When you have created all of the NFTs that you want, freeze the collection so it cannot be changed and no more NFTs can be added by running this command: + +```bash +tznft mint-freeze bob my_collection +``` + +## Mint tokens on a testnet + +So far, the NFTs that you have created are available only in your local sandbox. +When you are satisfied with the NFTs and how they behave, you can send them to a testnet and test them there. +You can use the same configuration files and IPFS data as you used on the sandbox. + +By default, the `tznft.json` file has configuration information for the Tezos Ghostnet testnet, where you can test your tokens on a running Tezos network. + +1. Show the available networks by running the command `tznft show-network --all` and verify that the testnet is in the list. + +1. Change the `tznft` tool to use the testnet instead of your local sandbox: + + ```bash + tznft set-network testnet + ``` + +1. Run the `tznft bootstrap` command to get the testnet ready for your use. +Now that the network is set to testnet, this command deploys a helper balance inspector contract to testnet that allows the `tznft` command to get information from the testnet. +You only need to run this command for testnet once. + +1. Create an alias on the testnet to own the NFTs. +You can do this in either of these two ways: + + - If you have an existing Tezos wallet that supports testnets (such as Temple wallet), copy the private key from that wallet and use the `tznft add-alias` command to create a local alias for it. + For example, this command creates a wallet with the alias `my-account`: + + ```bash + tznft add-alias my-account $TEZOS_PRIVATE_KEY + ``` + + - Create a local wallet with the installation of the `octez-client` command within the Flextesa Docker container: + + 1. Generate local keys with the `octez-client gen keys` command. + For example, this command creates keys for a wallet with the alias `my-account`: + + ```bash + docker exec flextesa-sandbox octez-client gen keys my-account + ``` + + 1. Get the keys for the wallet with this command: + + ```bash + docker exec flextesa-sandbox octez-client show address my-account -S + ``` + + The response includes the hash, public key, and secret key for the wallet. + For example, in this response, the secret key starts with "edsk3WR": + + ![The keys for the new account](/images/nft-create/new-key-output.png) + + 1. Add the secret key as an alias with the `tznft` command, replacing `$TEZOS_PRIVATE_KEY` with the value of the secret key from the previous command: + + ```bash + tznft add-alias my-account $TEZOS_PRIVATE_KEY + ``` + + 1. Add funds to the new wallet by going to the Ghostnet faucet at , pasting the wallet's hash in the "Or fund any address" field, and clicking a button to request tokens. + The wallet needs tokens to pay the fees to create the collection and mint the tokens on Ghostnet. + +1. Create the collection on the testnet. +The command is the same as for the sandbox, and you can create a new collection file or use the file from the sandbox. +Similarly, you can use the same collection alias because `tznft` keeps aliases separate on different networks, but be sure not to get the aliases confused. + + ```bash + tznft create-collection my-account --meta_file my_collection.json --alias my_collection + ``` + +1. Mint the tokens on the testnet. +The command is the same as for the sandbox: + + ```bash + tznft mint my-account my_collection --tokens '1, ipfs://abcde12345' + ``` + + You can add more NFTs until you freeze the collection. + +1. View your token balances. +The command is the same as for the sandbox: + + ```bash + tznft show-balance --nft my_collection --signer my-account --owner my-account --tokens 1 + ``` + +1. View the tokens on a block explorer: + + 1. Get the address of the collection on the testnet from the `testnet` section of the `tznft.json` file. + The address starts with "KT1". + + 1. Go to a block explorer, such as . + + 1. Set the block explorer to use testnet instead of Tezos mainnet. + + 1. In the search field, paste the address of the collection and press Enter. + + The block explorer shows information about the contract that manages the NFTs, including a list of all NFTs in the contract, who owns them, and a list of recent transactions. + +Now the NFTs are on Tezos ghostnet and you can transfer and manipulate them just like you did in the sandbox. +You may need to create and fund more account aliases to transfer them, but the commands are the same. +For example, to transfer NFTs to an account with the alias `other-account`, run this command: + +```bash +tznft transfer --nft my_collection --signer my-account --batch 'my-account, other-account, 1' 'my-account, other-account, 2' +``` + +## Summary + +Now you can create, test, and deploy NFTs locally and to testnets. +The process for minting NFTs to Tezos mainnet is the same, but you must use an account with real XTZ in it to pay the transaction fees. + +If you want to continue working with these NFTs, try creating a marketplace for them as described in the tutorial [Build an NFT Marketplace](../build-an-nft-marketplace). diff --git a/src/pages/tutorials/smart-rollups/debug.md b/src/pages/tutorials/smart-rollups/debug.md new file mode 100644 index 000000000..27532712f --- /dev/null +++ b/src/pages/tutorials/smart-rollups/debug.md @@ -0,0 +1,38 @@ +--- +id: debug +title: "Part 2: Running the kernel in debug mode" +lastUpdated: 25th October 2023 +--- + +Octez provides a command named `octez-smart-rollup-wasm-debugger` that runs smart rollups in debug mode to make it easier to test and observe them. +Later, you will deploy the rollup to the sandbox, but running it in debug mode first verifies that it built correctly. + +1. In the terminal window inside the Docker container, go to the `hello_world_kernel` folder. + +1. Run this command to start the rollup and pass an empty message inbox to it: + + ```bash + octez-smart-rollup-wasm-debugger \ + --kernel target/wasm32-unknown-unknown/debug/hello_world_kernel.wasm \ + --inputs empty_input.json + ``` + + The command prompt changes again to show that you are in debugging mode, which steps through commands. + +1. At the debugging prompt, run this command to send the message inbox to the kernel: + + ```bash + step inbox + ``` + + The response shows the logging information for the kernel, including these parts: + + - The message "Hello, kernel" from the `hello_kernel` function + - The message "Got message: Internal(StartOfLevel)," which represents the start of the message inbox + - The message "Got message: Internal(InfoPerLevel(InfoPerLevel ...," which provides the hash and timestamp of the previous block + - The message "Got message: Internal(EndOfLevel)," which represents the end of the message inbox + +1. Press Ctrl + C to end debugging mode. + +Now you know that the kernel works. +In the next section, you optimize the kernel to be deployed to the sandbox. diff --git a/src/pages/tutorials/smart-rollups/deploy.md b/src/pages/tutorials/smart-rollups/deploy.md new file mode 100644 index 000000000..c77ad5bb8 --- /dev/null +++ b/src/pages/tutorials/smart-rollups/deploy.md @@ -0,0 +1,66 @@ +--- +id: deploy +title: "Part 4: Deploying (originating) the rollup" +lastUpdated: 25th October 2023 +--- + +Smart rollups are originated in a way similar to smart contracts. +Instead of running the `octez-client originate contract` command, you run the `octez-client originate smart rollup` command. +This command creates an address for the rollup and stores a small amount of data about it on layer 1. + +1. In the Docker container, in the `hello-world-kernel` folder, run this command to start the sandbox: + + ```bash + ./sandbox_node.sh + ``` + + This command starts a Tezos testing environment, including a baking node running in sandbox mode and a group of test accounts. + The console shows repeated messages that show that the node is baking blocks. + For more information about sandbox mode, see [sandbox mode](https://tezos.gitlab.io/user/sandbox.html). + + If you see an error that says "Unable to connect to the node," you can ignore it because it happens only once while the node is starting. + +1. Leave that terminal instance running for the rest of the tutorial. + +1. Open a new terminal window. + +1. In the new terminal window, enter the Docker container by running this command: + + ```bash + docker exec -it octez-container /bin/sh + ``` + + Now the second terminal window is running inside the container just like the first one. + +1. In the second terminal window, run this command to verify that the sandbox is running with the correct protocol: + + ```bash + octez-client rpc get /chains/main/blocks/head/metadata | grep protocol + ``` + + The response shows the protocol that the sandbox is running, as in this example: + + ``` + { "protocol": "ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK", + "next_protocol": "ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK", + ``` + + If you don't see a message that looks like this one, check for errors in the first terminal window. + + Now the sandbox is running in the Docker container and you can use it to test the rollup. + +1. Run this command to deploy the installer kernel to the Tezos sandbox: + + ```bash + octez-client originate smart rollup \ + "test_smart_rollup" from "bootstrap1" \ + of kind wasm_2_0_0 of type bytes \ + with kernel file:hello_world_kernel_installer.hex --burn-cap 3 + ``` + + If you need to open a new terminal window within the Docker container, run the command `docker exec -it octez-container /bin/sh`. + + Like the command to originate a smart contract, this command uses the `--burn-cap` argument to allow the transaction to take fees from the account. + Also like deploying a smart contract, the response in the terminal shows information about the transaction and the address of the originated smart rollup, which starts with `sr1`. + +Now layer 1 is aware of the rollup and nodes can run the rollup kernel. diff --git a/src/pages/tutorials/smart-rollups/index.md b/src/pages/tutorials/smart-rollups/index.md index 3ce166f22..3dcd55a6e 100644 --- a/src/pages/tutorials/smart-rollups/index.md +++ b/src/pages/tutorials/smart-rollups/index.md @@ -1,537 +1,235 @@ --- -id: smart-rollups-tutorial -title: Smart Rollups Onboarding Tutorial -lastUpdated: 7th July 2023 +id: smart-rollups +title: Introduction +lastUpdated: 11th October 2023 --- +This tutorial covers how to deploy a smart rollup in a Tezos sandbox. +To run this tutorial, you should have a basic understanding of how Tezos works and the ability to use the command-line terminal on your computer. -{% callout type="note" title="Repo Link" %} -Please clone this [repo](https://gitlab.com/trili/hello-world-kernel) to get started. -{% /callout %} +In this tutorial, you will learn: -## Introduction +- What a smart rollup is and how they help scale Tezos +- How information passes between Tezos and smart rollups +- How to respond to messages from Tezos in a smart rollup -- `src/lib.rs` -- contains the `Rust` code for our "Hello, World" kernel. -- `Cargo.toml` -- has the necessary dependencies for the building process. -- `empty_input.json` -- an empty example of a kernel input (for debugging purposes). -- `rustup-toolchain.toml` -- specifies the `rust` version required. -- `sandbox_node.sh` -- the script for setting up the sandboxed mode binaries. -- `two_inputs.json` -- an example of a kernel input with two messages (for debugging purposes). +{% comment %} +It would be good to add: +- How to send messages from a smart rollup to Tezos +{% /comment %} -This tutorial will explain in detail the necessary steps for setting up a **smart rollup** on a test network for the Tezos blockchain. A valuable resource for learning about this exciting new feature can be found by following the [GitLab documentation](https://tezos.gitlab.io/alpha/smart_rollups.html). +## What is a smart rollup? -This is an attempt to collect information from multiple resources (provided at the end of this tutorial) to ensure a smooth onboarding experience. However, a basic familiarity with blockchain terms and practices is assumed. +Smart rollups are processing units that run outside the Tezos network but communicate with Tezos on a regular basis. +These processing units can run arbitrarily large amounts of code without waiting for Tezos baking nodes to run and verify that code. +Smart rollups use Tezos for information and transactions but can run large applications at their own speed, independently of the Tezos baking system. -The article is available on the official Tezos developers' website, at the section dedicated to [smart rollups](https://tezos.com/developers/smart-rollups/). +In this way, smart rollups allow Tezos to scale to support large, complex applications without slowing Tezos itself. +The processing that runs on Tezos itself via smart contracts is referred to as _layer 1_ and the processing that smart rollups run is referred to as _layer 2_. +To learn about running code in smart contracts, see the tutorial [Deploy a smart contract](../deploy-your-first-smart-contract). -## 1. Introduction to Smart Rollups +Rollups also have an outbox, which consists of calls to smart contracts on layer 1. +These calls are how rollups send messages back to Tezos. -**Smart rollups** are an elegant solution for **horizontally scaling** the Tezos blockchain, which involves distributing the workload of the main layer (Layer 1) to external layers that perform their tasks "off-chain". In comparison, **vertical scaling** focuses on optimizing the main layer itself but is less scalable than the former. +Smart rollups can run any kind of applications that they want, such as: -Let us use an analogy: think of a company with N employees who receive increasing amounts of work every day. Eventually, the team becomes overwhelmed. While hiring more people for the team is an option, it becomes challenging for them to coordinate, which ultimately reduces their productivity. +- Financial applications that use information and transactions from Tezos +- Gaming applications that manipulate assets and keep them in sync with Tezos +- Applications that run complex logic on NFTs or other types of tokens +- Applications that communicate with other blockchains -**Vertical scaling** in this scenario means providing better working equipment to increase productivity. On the other hand, **horizontal scaling** involves creating external teams that work on specific portions of the workload, reducing the need for extensive interaction with the initial team. The latter option is more scalable because one can continuously improve the equipment up to a certain point, while creating external teams can happen at any time and will always be beneficial. In our case, these external teams are the **smart rollups**. +{% comment %} +TODO Should this intro discuss the reveal data channel? +{% /comment %} -## 2. The Kernel +Rollups maintain consensus by publishing the hash of their state to Tezos, which other nodes can use to verify the rollup's behavior. +The specific way that rollups publish their states and maintain consensus is beyond the scope of this tutorial. +For more information about rollups and their consensus mechanism, see [Smart Optimistic Rollups](../../advanced-topics/smart-rollups/). -### 2.1. Definition +This diagram shows a smart rollup interacting with layer 1 by receiving a message, running processing based on that message, and sending a transaction to layer 1: -The core component of any smart rollup is the **kernel**. A kernel is a 32-bit `WebAssembly` (`WASM`) program responsible for managing input messages, updating the state of the rollup, and determining when to output messages to Layer 1. To continue with the analogy, the kernel represents the work ethic of the "external team". +{% html htmlWrapperTag="div" %} -### 2.2. `Rust` +
-In this tutorial, `Rust` is used as the programming language for the kernel due to its excellent support for `WASM`. However, any programming language that has `WASM` compilation support could be used. +{% /html %} -Prerequisites for developing kernels are `cargo` and a `Rust` compiler with `WebAssembly` support (e.g. `wasm32-unknown-unknown` target). +Smart rollups stay in sync with Tezos by passing messages to Tezos and receiving messages from Tezos and other rollups. +Each Tezos block contains a global rollups inbox that contains messages from Tezos layer 1 to all rollups. +Anyone can add a message to this inbox and all messages are visible to all rollups. +Rollups receive this inbox, filter it to the messages that they are interested in, and act on them accordingly. -We propose using `rustup` for this purpose by following this [installation tutorial](https://www.rust-lang.org/tools/install): +## Smart rollup analogy -```bash! -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -``` - -### 2.3. `Clang` and `LLVM` - -We need `Clang` for compilation to `WebAssembly`. At minimum version `11` is required. Here are some suggested ways to achieve that, depending on your OS: - -#### MacOS - -```bash! -brew install llvm -export CC="$(brew --prefix llvm)/bin/clang" -``` - -#### Ubuntu - -```bash! -sudo apt-get install clang-11 -export CC=clang-11 -``` - -#### Fedora - -```bash! -dnf install clang -export CC=clang -``` - -#### Arch Linux - -```bash! -pacman -S clang -export CC=clang -``` - -We do `export CC` because there are systems, such as various Linux distributions, that don't ship with `Clang` as their default `C/C++` compiler. - -Check that at least version `11` is installed with `$CC --version`. - -Also, ensure that the `clang` you've installed supports the `wasm32` target with: - -```bash! -$CC -print-targets | grep WebAssembly -# wasm32 - WebAssembly 32-bit -# wasm64 - WebAssembly 64-bit -``` - -#### `AR` on macOS - -To compile to `WebAssembly` on macOS, you need to use the `LLVM` archiver. If you've used `Homebrew` to install `LLVM`, you can configure it with the following: - -```bash! -export AR="$(brew --prefix llvm)/bin/llvm-ar" -``` - -### 2.4. WebAssembly Toolkit - -During development, having the [`WebAssembly Toolkit` (`wabt`)](https://github.com/WebAssembly/wabt) available is useful. It provides tooling for stripping `WebAssembly` binaries (`wasm-strip`) and conversion utilities between the textual and binary representations of `WebAssembly` (`wat2wasm`, `wasm2wat`). - -Most distributions ship a `wabt` package, which you can install using: - -#### MacOS - -```bash! -brew install wabt -``` - -#### Ubuntu - -```bash! -sudo apt install wabt -``` - -#### Fedora - -```bash! -dnf install wabt -``` - -#### Arch Linux - -```bash! -pacman -S wabt -``` - -Then, check that the `wasm-strip` version is at least `1.0.31` (with `wasm-strip --version`). If not, you can download it directly from [here](https://github.com/WebAssembly/wabt/releases/tag/1.0.31), extract the files, and then whenever you have to use `wasm-strip`, you can use `./bin/wasm-strip` instead. - -### 2.5. "Hello, World!" Kernel - -To get started, we've prepared a [repository](https://gitlab.com/trili/hello-world-kernel) that helps you get started with kernel development quickly. - -You can clone the repository as follows: - -```bash! -git clone https://gitlab.com/trili/hello-world-kernel.git -cd hello-world-kernel/ -``` - -Now, ensure that you have the `rust` version (run `cargo --version`) at least `1.66.0` installed. Otherwise, run the following: - -```bash! -rustup override set 1.66.0 -``` - -With `rustup`, you have to enable `WASM` as a compilation target using the following: - -```bash! -rustup target add wasm32-unknown-unknown -``` - -You can now immediately build using: - -```bash! -cargo build --target wasm32-unknown-unknown -``` - -After building it, you should be able to inspect the produced artifacts. - -```bash! -ls -1 target/wasm32-unknown-unknown/debug -# build -# deps -# examples -# hello_world_kernel.d -# hello_world_kernel.wasm -# incremental -# libhello_world_kernel.d -# libhello_world_kernel.rlib -``` - -The most important item is `hello_world_kernel.wasm`, which is our readily compiled kernel. - -## 3. Getting `Octez` - -You need the `Octez` binaries to test locally and deploy a Smart Rollup kernel. `Octez` is distributed in multiple ways. In this tutorial, we strongly encourage using [Docker](https://www.docker.com/). - -The [Octez container images](https://hub.docker.com/r/tezos/tezos/) are automatically generated from the [Tezos GitLab repository](https://gitlab.com/tezos/tezos), ensuring that you can always access the latest version of the `Octez` binaries. - -To obtain the most recent image from our repository, execute the following command: - -```bash! -docker pull tezos/tezos:master -``` - -Now, you can initiate an interactive (`-it`) session with `Docker` based on that image, which allows access to the kernel files created as part of this tutorial. To achieve this, you must mount the current directory (you must be in the `"Hello, World!" kernel` directory) within the container using the [`--volume`](https://docs.docker.com/storage/bind-mounts/) argument. Run the following command within the `"Hello, World!" kernel` directory: - -```bash! -docker run -it --rm --volume $(pwd):/home/tezos/hello-world-kernel --entrypoint /bin/sh --name octez-container tezos/tezos:master -``` - -The `--rm` option is used so that the container that we created will be killed when we exit the `Docker` session. - -In the rest of the tutorial, we will have to do work both inside and outside the `Docker` section(s). For clarity, we will specify where the commands -should be executed. The command above means we are now in `docker session 1`. - -At this point, you should observe that the `"Hello, World!" kernel` directory is accessible and contains the kernel files previously created. - -`docker session 1` - -```bash! -ls -1 hello-world-kernel -# same contents as in the hello-world-kernel repository -``` - -At this stage, you can verify that the container image includes all the required executables: - -`docker session 1` +Businesses talk about _horizontal scaling_ versus _vertical scaling_. +If a business is growing and its employees are being overworked, the business could use vertical scaling to hire more employees or use better tools to improve the productivity of each employee. +Scaling Tezos in this way would mean using more processing power to process each new block, which would increase the cost to run baking nodes. +Also, if the business hires more employees, the amount of communication between employees increases because, for example, they have to make sure that they are working in the same way and not doing duplicate jobs. -```bash! -octez-node --version -# 6fb8d651 (2023-06-05 12:05:17 +0000) (0.0+dev) -octez-smart-rollup-wasm-debugger --version -# 6fb8d651 (2023-06-05 12:05:17 +0000) (0.0+dev) -octez-smart-rollup-node-alpha --version -# 6fb8d651 (2023-06-05 12:05:17 +0000) (0.0+dev) -octez-client --version -# 6fb8d651 (2023-06-05 12:05:17 +0000) (0.0+dev) -``` - -Please note that the version number mentioned may not precisely match the version you have locally, as the container images are periodically updated. - -## 4. Processing the Kernel - -### 4.1. Debugging the Kernel - -Before originating a rollup, it can be helpful to observe the behavior of its kernel. To facilitate this, there is a dedicated `Octez` binary called `octez-smart-rollup-wasm-debugger`. -However, before using it, it is important to understand how the rollup receives its inputs. Each block at every level of the blockchain has a specific section dedicated to the shared and unique **smart rollup inbox**. Consequently, the inputs of a rollup can be seen as a list of inboxes for each level, or more precisely, a list of lists. -Let us start with a trivial inbox, which is stored in the `empty_input.json` file. We can debug the `"Hello, World!" kernel` with: - -`docker session 1` - -```bash! -cd hello-world-kernel -``` - -`docker session 1` - -```bash! -octez-smart-rollup-wasm-debugger --kernel target/wasm32-unknown-unknown/debug/hello_world_kernel.wasm --inputs empty_input.json -``` - -Now you are in **debugging** mode, which is very well documented and explained in the [documentation](https://tezos.gitlab.io/alpha/smart_rollups.html#testing-your-kernel). Similar to how the rollup awaits internal messages from Layer 1 or external sources, the debugger also waits for inputs. - -Once we're in the debugger REPL (read–eval–print loop), you can run the kernel for one level using the `step inbox` command: - -`docker session 1` - -```bash! -> step inbox -# Loaded 0 inputs at level 0 -# Hello, kernel! -# Got message: Internal(StartOfLevel)! -# Got message: Internal(InfoPerLevel(InfoPerLevel { predecessor_timestamp: 1970-01-01T00:00:00Z, predecessor: BlockHash("BKiHLREqU3JkXfzEDYAkmmfX48gBDtYhMrpA98s7Aq4SzbUAB6M") }))! -# Got message: Internal(EndOfLevel)! -# Evaluation took 11000000000 ticks so far -# Status: Waiting for input -# Internal_status: Collect -``` - -Let us explain what our kernel is supposed to do: - -- whenever it receives an input, it prints the `"Hello, kernel!"` message. -- whenever there is a message in the input, it is printed, because of the `handle_message` function. - -It is important to understand that the **shared rollup inbox** has at each level at least the following **internal** messages: - -- `StartOfLevel` -- marks the beginning of the inbox level and does not have any payload. -- `InfoPerLevel` -- provides the timestamp and block hash of the predecessor of the current Tezos block as payload. -- `EndOfLevel` -- pushed after the application of the operations of the Tezos block and does not have any payload. - -You will notice that the behavior aligns with the expectations. You can also experiment with a non-empty input, such as `two_inputs.json`: - -`docker session 1` - -```bash! -octez-smart-rollup-wasm-debugger --kernel target/wasm32-unknown-unknown/debug/hello_world_kernel.wasm --inputs two_inputs.json -``` - -`docker session 1` - -```bash! -> step inbox -# Loaded 2 inputs at level 0 -# Hello, kernel! -# Got message: Internal(StartOfLevel) -# Got message: Internal(InfoPerLevel(InfoPerLevel { predecessor_timestamp: 1970-01-01T00:00:00Z, predecessor: BlockHash("BKiHLREqU3JkXfzEDYAkmmfX48gBDtYhMrpA98s7Aq4SzbUAB6M") })) -# Got message: External([26, 84, 104, 105, 115, 32, 109, 101, 115, 115, 97, 103, 101, 32, 105, 115, 32, 102, 111, 114, 32, 109, 101]) -# Got message: External([5, 84, 104, 105, 115, 32, 111, 110, 101, 32, 105, 115, 110, 39, 116]) -# Got message: Internal(EndOfLevel) -# Evaluation took 11000000000 ticks so far -# Status: Waiting for input -# Internal_status: Collect -``` - -As expected, the two messages from the input are also displayed as debug messages. -Feel free to explore additional examples from the dedicated [kernel gallery](https://gitlab.com/tezos/kernel-gallery/-/tree/main/) or create your own! - -### 4.2. Reducing the Size of the Kernel - -The origination process is similar to that of smart contracts. To originate a smart rollup, we have to consider the size of the kernel that will be deployed. The size of the kernel needs to be smaller than the manager operation size limit. - -Regrettably, the size of the `.wasm` file is currently too large: - -`docker session 1` - -```bash! -du -h target/wasm32-unknown-unknown/debug/hello_world_kernel.wasm -# 17.3M target/wasm32-unknown-unknown/debug/hello_world_kernel.wasm -``` - -To address this, we can use `wasm-strip`, a tool designed to reduce the size of kernels. It accomplishes this by removing unused parts of the `WebAssembly` module (e.g. dead code), which are not required for the execution of the rollups. Open a new terminal session and navigate to the `"Hello, world!" kernel ` directory since you do not have `wasm-strip` in your `Docker` session: - -`outside docker session - hello-world-kernel` - -```bash! -wasm-strip target/wasm32-unknown-unknown/debug/hello_world_kernel.wasm - -du -h target/wasm32-unknown-unknown/debug/hello_world_kernel.wasm -# 532.0K target/wasm32-unknown-unknown/debug/hello_world_kernel.wasm -``` +By contrast, smart rollups behave like horizontal scaling. +In horizontal scaling, businesses create specialized teams that work on different portions of the workload. +These teams can work independently of other teams and take advantage of efficiencies of being focused on a specific task. +They also need to communicate less with other teams, which speeds up their work. +Smart rollups are like separate horizontally scaled teams, with Tezos layer 1 as the source of communication between teams. -The modifications from outside will get propagated to the interactive `Docker` session thanks to the `--volume` command option. +## Prerequisites -Undoubtedly, this process has effectively reduced the size of the kernel. However, there is still additional work required to ensure compliance with the manager operation size limit. +To run this tutorial, make sure that the following tools are installed: -### 4.3. The Installer Kernel +{% comment %} +TODO Could we install some of the tools in the docker container instead of making them install them locally? +{% /comment %} -Instead of using a kernel file for origination in the aforementioned format, an alternative approach is to utilize the **installer** version of the kernel. This **installer kernel** can be **upgraded** to the original version if provided with additional information in the form of **preimages**, which can be provided to the rollup node later on as part of its **reveal data channel**. +- [Docker](https://www.docker.com/) -There are two ways to communicate with smart rollups: +- Rust -1. **global inbox** -- allows Layer 1 to transmit information to all rollups. This unique inbox contains two kinds of messages: **external messages** are pushed through a Layer 1 manager operation, while **internal messages** are pushed by Layer 1 smart contracts or the protocol itself (e.g. `StartOfLevel`, `InfoPerLevel`, `EndOfLevel`). -2. **reveal data channel** -- allows the rollup to retrieve data (e.g. **preimages**) coming from data sources external to Layer 1. + The application in this tutorial uses Rust because of its support for WebAssembly (WASM), the language that smart rollups use to communicate. + Rollups can use any language that has WASM compilation support. -The main benefit of the installer kernel is that it is small enough to be used in origination regardless of the kernel that it will be upgraded to. + To install Rust via the `rustup` command, run this command: -There is an [installer kernel origination topic](https://tezos.stackexchange.com/questions/4784/how-to-originating-a-smart-rollup-with-an-installer-kernel/5794#5794) for this; please consult it for further clarifications. To generate the **installer kernel**, the `smart-rollup-installer` tool is required: + ```bash + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + ``` -`outside docker session - hello-world-kernel` + You can see other ways of installing Rust at . -```bash! -cargo install tezos-smart-rollup-installer -``` +- Clang and LLVM -To create the installer kernel from the initial kernel: + Clang and LLVM are required for compilation to WebAssembly. + Version 11 or later of Clang is required. + Here are instructions for installing the appropriate tools on different operating systems: -`outside docker session - hello-world-kernel` + **MacOS** -```bash! -smart-rollup-installer get-reveal-installer --upgrade-to target/wasm32-unknown-unknown/debug/hello_world_kernel.wasm --output hello_world_kernel_installer.hex --preimages-dir preimages/ -``` + ```bash + brew install llvm + export CC="$(brew --prefix llvm)/bin/clang" + ``` -This command creates the following: + **Ubuntu** -- `hello_world_kernel_installer.hex` -- the hexadecimal representation of the installer kernel to be used in the origination. -- `preimages/` -- a directory containing the preimages necessary for upgrading from the installer kernel to the original kernel. These preimages are transmitted to the rollup node that runs the installer kernel with the help of the [**reveal data channel**](https://tezos.gitlab.io/alpha/smart_rollups.html#reveal-data-channel). + ```bash + sudo apt-get install clang-11 + export CC=clang-11 + ``` -Notice the reduced dimensions of the installer kernel: + **Fedora** -`outside docker session - hello-world-kernel` + ```bash + dnf install clang + export CC=clang + ``` -```bash! -du -h hello_world_kernel_installer.hex -# 36.0K hello_world_kernel_installer.hex -``` + **Arch Linux** -Because of the size of this installer kernel, you are now ready for deployment. + ```bash + pacman -S clang + export CC=clang + ``` -Note that this shows the size of the `hex` encoded file, which is larger than the actual binary size of the kernel that we originate. + The `export CC` command sets Clang as the default C/C++ compiler. -## 5. Deploying the Kernel + After you run these commands, run `$CC --version` to verify that you have version 11 or greater installed. -### 5.1. Sandboxed Mode + Also, ensure that your version of Clang `wasm32` target with by running the command `$CC -print-targets | grep wasm32` and verifying that the results include `wasm32`. -Our goal now is to create a testing environment for originating our rollup with the created kernel. In the `hello-world-kernel` repository, we offer the `sandbox-node.sh` file, which does the following: +- AR (macOS only) -- configures the `Octez` node to operate in [**sandbox mode**](https://tezos.gitlab.io/user/sandbox.html). -- activates the `alpha` protocol by using an `activator` account. -- creates five test (bootstrapping) accounts used for manual [**baking**](https://opentezos.com/baking/cli-baker/). -- creates a loop of continuous baking. + To compile to WebAssembly on macOS, you need to use the LLVM archiver. + If you've used Homebrew to install LLVM, you can configure it to use the archiver by running this command: -Run the file with: + ```bash + export AR="$(brew --prefix llvm)/bin/llvm-ar" + ``` -`docker session 1` +- WebAssembly Toolkit -```bash! -./sandbox_node.sh -``` + The the [WebAssembly Toolkit (`wabt`)](https://github.com/WebAssembly/wabt) provides tooling for reducing (or _stripping_) the size of WebAssembly binaries (with the `wasm-strip` command) and conversion utilities between the textual and binary representations of WebAssembly (including the `wat2wasm` and `wasm2wat` commands). -Ignore the "Unable to connect to the node" error, as it only comes once because the `octez-client` command was used while the node was not yet bootstrapped. The result should be a permanent loop containing: + Most distributions ship a `wabt` package, which you can install with the appropriate command for your operating system: -`docker session 1` + **MacOS** -```bash! -# Injected block at minimal timestamp -``` + ```bash + brew install wabt + ``` -Leave that process running. Open a new `Docker` session, which works in the same container named `octez-container`: + **Ubuntu** -`outside docker session - hello-world-kernel` + ```bash + sudo apt install wabt + ``` -```bash! -docker exec -it octez-container /bin/sh -``` + **Fedora** -It is very important to remember to open a new terminal session and run the command above whenever we mention a "new `Docker` session" or when you see that the `docker session` counter has increased. + ```bash + dnf install wabt + ``` -To check that the network has the correctly configured protocol: + **Arch Linux** -`docker session 2` + ```bash + pacman -S wabt + ``` -```bash! -octez-client rpc get /chains/main/blocks/head/metadata | grep protocol + To verify that `wabt` is installed, run the command `wasm-strip --version` and verify that the version is at least 1.0.31. + If not, you can download this version directly and extract its files: . + Then, whenever you have to use `wasm-strip`, you can use `./bin/wasm-strip` instead. -# "protocol": "ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK", -# "next_protocol": "ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK" -``` +## Tutorial application -You are now ready for the Smart Rollup origination process. +Despite the number of command-line tools needed, the code for the core of the rollup itself is relatively simple. +This core is called the _kernel_ and is responsible for accepting messages from layer 1 and sending messages to layer 1. -### 5.2. Smart Rollup Origination +The code for the tutorial application is here: . -To originate a smart rollup using the `hello_world_kernel_installer` created above: +The code for the kernel is in the `src/lib.rs` file. +It is written in the Rust programming language and looks like this: -`docker session 2` +```rust +use tezos_smart_rollup::inbox::InboxMessage; +use tezos_smart_rollup::kernel_entry; +use tezos_smart_rollup::michelson::MichelsonBytes; +use tezos_smart_rollup::prelude::*; -```bash! -octez-client originate smart rollup "test_smart_rollup" from "bootstrap1" of kind wasm_2_0_0 of type bytes with kernel file:hello-world-kernel/hello_world_kernel_installer.hex --burn-cap 3 -``` +kernel_entry!(hello_kernel); -```bash! -# > Node is bootstrapped. -# ... -# Smart rollup sr1B8HjmEaQ1sawZtnPU3YNEkYZavkv54M4z memorized as "test_smart_rollup" -``` +fn handle_message(host: &mut impl Runtime, msg: impl AsRef<[u8]>) { + if let Some((_, msg)) = InboxMessage::::parse(msg.as_ref()).ok() { + debug_msg!(host, "Got message: {:?}\n", msg); + } +} -In the command above, the `--burn-cap` option specifies the amount of ꜩ you are willing to "burn" (lose) to allocate storage in the global context of the blockchain for each rollup. +pub fn hello_kernel(host: &mut impl Runtime) { + debug_msg!(host, "Hello, kernel!\n"); -To run a rollup node for the rollup using the installer kernel, you need to copy the contents of the preimages directory to `${ROLLUP_NODE_DIR}/wasm_2_0_0/`. You can set `$ROLLUP_NODE_DIR` to `~/.tezos-rollup-node`, for instance: - -`docker session 2` - -```bash! -mkdir -p ~/.tezos-rollup-node/wasm_2_0_0 - -cp hello-world-kernel/preimages/* ~/.tezos-rollup-node/wasm_2_0_0/ + while let Some(msg) = host.read_input().unwrap() { + handle_message(host, msg); + } +} ``` -You should now be able to **run** your rollup node: - -`docker session 2` - -```bash! -octez-smart-rollup-node-alpha run operator for "test_smart_rollup" with operators "bootstrap2" --data-dir ~/.tezos-rollup-node/ --log-kernel-debug --log-kernel-debug-file hello_kernel.debug -``` - -Leave this running as well, and open another `Docker` session, as already explained, with the `octez-container`. - -Each time a block is baked, a new "Hello, kernel!" message should appear in the `hello_kernel.debug` file: - -`docker session 3` - -```bash! -tail -f hello_kernel.debug -# Hello, kernel! -# Got message: Internal(StartOfLevel) -# Got message: Internal(InfoPerLevel(InfoPerLevel { predecessor_timestamp: 2023-06-07T15:31:09Z, predecessor: BlockHash("BLQucC2rFyNhoeW4tuh1zS1g6H6ukzs2DQDUYArWNALGr6g2Jdq") })) -# Got message: Internal(EndOfLevel) -# ... (repeats) -``` - -Finally, you have successfully deployed a very basic yet functional smart rollup. - -### 5.3. Sending an Inbox Message to the Smart Rollup - -We now want to send an external message into the rollup inbox, which should be read by our kernel and sent as a debug message. First, we will wait for it to appear using: - -`docker session 3` - -```bash! -tail -f hello_kernel.debug | grep External -``` - -Open yet another `Docker` session and send an external message into the rollup inbox. You can use the `Octez` client: - -`docker session 4` - -```bash! -octez-client send smart rollup message '[ "test" ]' from "bootstrap3" -``` - -Once you send the Smart Rollup message, you will notice that in the debug trace, you get: - -`docker session 3` - -```bash! -Got message: External([116, 101, 115, 116]) -``` - -`116, 101, 115, 116` represent the bytes of "test". - -### 5.4. Test Networks +This example kernel has these major parts: -In the above section, we proposed how to create your `Octez` binaries in **sandbox mode**. Here, we propose a different approach to that, using [test networks](https://teztnets.xyz/). We encourage the reader to try at least one of the following linked tutorials: +1. It imports resources that allow it to access and decode messages from layer 1. +1. It runs the Rust macro `kernel_entry!` to set the main function for the kernel. +1. It declares the `handle_message` function, which accepts, decodes, and processes messages from layer 1. +In this case, the function decodes the message (which is sent as a sequence of bytes) and prints it to the log. +The function could call any other logic that the application needs to run. +1. It declares the `hello_kernel` function, which is the main function for the kernel. +It runs each time the kernel receives messages from layer 1, prints a logging message each time it is called, and runs the `handle_message` function on each message. -- [Ghostnet](https://teztnets.xyz/ghostnet-about) -- uses the protocol that `Mainnet` follows as well. -- [Nairobinet](https://teztnets.xyz/nairobinet-about) -- uses the `Nairobi` protocol. -- [Mondaynet](https://teztnets.xyz/mondaynet-about) -- uses the `alpha` protocol and resets every Monday. +You don't need to access the other files in the application directly, but here are descriptions of them: -The workflow should be similar to the one presented for the sandbox mode: +- `src/lib.rs`: The Rust code for the kernel +- `Cargo.toml`: The dependencies for the build process +- `rustup-toolchain.toml`: The required Rust version +- `sandbox_node.sh`: A script that sets up a Tezos sandbox for testing the rollup -- **configure** the network; -- run a node (needs to synchronize with the network -- can make use of [snapshots](https://tezos.gitlab.io/user/snapshots.html)); -- create test accounts (which should be funded by the appropriate **Faucet**); -- originate the rollup; -- run the rollup node; -- check the debug file. +The tutorial repository also includes two files that represent example message inboxes in layer 1 blocks: -## 6. Further References and Documentation +- `empty_input.json`: An empty rollup message inbox +- `two_inputs.json`: A rollup message inbox with two messages -1. [Smart Rollup Documentation](https://tezos.gitlab.io/alpha/smart_rollups.html) -2. [Smart Rollup Kernel SDK Tutorial](https://gitlab.com/tezos/tezos/-/tree/master/src/kernel_sdk) -3. [Smart Rollup Kernel Examples](https://gitlab.com/tezos/kernel-gallery/-/tree/main/) -4. [Ghostnet Indexer](https://ghost.tzstats.com/) -5. [Blockchain Explorer](https://ghostnet.tzkt.io/) -6. [Tezos Smart Rollups Resources](https://airtable.com/shrvwpb63rhHMiDg9/tbl2GNV1AZL4dkGgq) -7. [Tezos Testnets](https://teztnets.xyz/) -8. [Origination of the Installer Kernel](https://tezos.stackexchange.com/questions/4784/how-to-originating-a-smart-rollup-with-an-installer-kernel/5794#5794) -9. [Docker Documentation](https://docs.docker.com/get-started/) +When you're ready, move to the next section to begin setting up the application. diff --git a/src/pages/tutorials/smart-rollups/optimize.md b/src/pages/tutorials/smart-rollups/optimize.md new file mode 100644 index 000000000..6037d1872 --- /dev/null +++ b/src/pages/tutorials/smart-rollups/optimize.md @@ -0,0 +1,105 @@ +--- +id: optimize +title: "Part 3: Optimizing the kernel" +lastUpdated: 25th October 2023 +--- + +To originate the kernel on Tezos, it must fit within the maximum size for a layer 1 operation (32KB). +In these steps, you reduce the size of the kernel: + +1. Run this command to print the current size of the kernel: + + ```bash + du -h target/wasm32-unknown-unknown/debug/hello_world_kernel.wasm + ``` + + You can run this command inside or outside of the Docker container. + + Because you ran it in debug mode, the size of the compiled kernel and its dependencies may be 18MB or more, which is too large to originate. + +1. In a terminal window outside of the Docker container, run this command to create a release build of the kernel: + + ```bash + cargo build --release --target wasm32-unknown-unknown + ``` + +1. Check the size of the release build of the kernel: + + ```bash + du -h target/wasm32-unknown-unknown/release/hello_world_kernel.wasm + ``` + + The release build is significantly smaller, but still too large. + +1. In a terminal window outside of the Docker container, run the `wasm-strip` command to reduce the size of the kernel: + + ```bash + wasm-strip target/wasm32-unknown-unknown/release/hello_world_kernel.wasm + ``` + + This command removes WebAssembly code that is not necessary to run rollups. + You must run this command outside of the Docker container because it does not have the `wasm-strip` command. + +1. Run the `du` command again to see the new size of the kernel: + + ```bash + du -h target/wasm32-unknown-unknown/release/hello_world_kernel.wasm + ``` + + The size of the kernel is smaller now. + Note that the changes that you make to the kernel outside of the Docker container also appear in the container and vice versa because the folder is mounted with the Docker `--volume` argument. + + To get the kernel running with an even smaller size, you can use the installer kernel, which includes only enough information to install your original kernel. + To do this, your kernel is split up and stored in separate files called preimages. + Then you run the installer kernel, it requests these files and reconstructs the original kernel. + +1. Outside of the Docker container, run this command to install the installer kernel tool: + + ```bash + cargo install tezos-smart-rollup-installer + ``` + +1. Outside of the Docker container, run this command to create an installer kernel: + + ```bash + smart-rollup-installer get-reveal-installer \ + --upgrade-to target/wasm32-unknown-unknown/release/hello_world_kernel.wasm \ + --output hello_world_kernel_installer.wasm --preimages-dir preimages/ + ``` + + This command creates the following files: + + - `hello_world_kernel_installer.wasm`: The installer kernel + - `preimages/`: A directory that contains the preimages that allow nodes to restore the original kernel code + + When a node runs the installer kernel, it retrieves the preimages through the reveal data channel, a channel that smart rollups use to request data from outside of layer 1. + For more information about the reveal data channel, see [reveal data channel](https://tezos.gitlab.io/alpha/smart_rollups.html#reveal-data-channel). + +1. Verify the size of the installer kernel by running this command: + + ```bash + du -h hello_world_kernel_installer.wasm + ``` + + Now the kernel is small enough to originate on layer 1. + +1. Inside of the Docker container, run the installer kernel in debug mode by running this command: + + ```bash + octez-smart-rollup-wasm-debugger --kernel hello_world_kernel_installer.wasm \ + --preimage-dir preimages/ --inputs empty_input.json + ``` + + Then you can use the `step inbox` command to simulate receiving the inbox from layer 1. + You can see the hello world kernel messages in the log, which shows that the upgrade from the installer kernel to the full kernel was successful. + Press Ctrl + C to end the debugging session. + +1. Create a hexadecimal version of the installer kernel by running this command outside of the Docker container: + + ```bash + smart-rollup-installer get-reveal-installer \ + --upgrade-to target/wasm32-unknown-unknown/release/hello_world_kernel.wasm \ + --output hello_world_kernel_installer.hex --preimages-dir preimages/ + ``` + + In the next section, you originate this hex-encoded installer kernel on layer 1. diff --git a/src/pages/tutorials/smart-rollups/run.md b/src/pages/tutorials/smart-rollups/run.md new file mode 100644 index 000000000..0f4f84bad --- /dev/null +++ b/src/pages/tutorials/smart-rollups/run.md @@ -0,0 +1,116 @@ +--- +id: run +title: "Part 5: Running and interacting with the rollup node" +lastUpdated: 25th October 2023 +--- + +Now that the smart rollup is originated on layer 1, anyone can run a smart rollup node for it. +Smart rollup nodes are similar to baking nodes, but they run the smart rollup kernel instead of baking Tezos blocks. +In these steps, you start a smart rollup node, but note that anyone can run a node based on your kernel, including people who want to verify the rollup's behavior. + +## Running a smart rollup node + +1. Copy the contents of the `preimages` folder to a folder that the rollup node can access by running these commands: + + ```bash + mkdir -p ~/.tezos-rollup-node/wasm_2_0_0 + + cp preimages/* ~/.tezos-rollup-node/wasm_2_0_0/ + ``` + +1. In the second terminal window, in the Docker container, start the rollup node: + + ```bash + octez-smart-rollup-node-alpha run operator for "test_smart_rollup" \ + with operators "bootstrap2" --data-dir ~/.tezos-rollup-node/ \ + --log-kernel-debug --log-kernel-debug-file hello_kernel.debug + ``` + + Now the node is running and writing to the log file `hello_kernel.debug`. + Leave this command running in the terminal window just like you left the first terminal window running the Tezos sandbox. + +## Interacting with the rollup node + +Now you can add messages to the inbox and see the rollup node receive and respond to them. + +1. Open a third terminal window and enter the Docker container again: + + ```bash + docker exec -it octez-container /bin/sh + ``` + +1. In the container, go to the `hello_world_kernel` folder. + +1. Print the contents of the log file: + + ```bash + tail -f hello_kernel.debug + ``` + + Now, each time a block is baked, the smart rollup node prints the contents of the messages in the smart rollup inbox, as in this example: + + ``` + # Hello, kernel! + # Got message: Internal(StartOfLevel) + # Got message: Internal(InfoPerLevel(InfoPerLevel { predecessor_timestamp: 2023-06-07T15:31:09Z, predecessor: BlockHash("BLQucC2rFyNhoeW4tuh1zS1g6H6ukzs2DQDUYArWNALGr6g2Jdq") })) + # Got message: Internal(EndOfLevel) + ``` + +1. Stop the command by pressing Ctrl + C. + +1. Run this command to watch for external messages to the rollup: + + ```bash + tail -f hello_kernel.debug | grep External + ``` + + No output appears at first because the rollup has not received any messages aside from the internal messages that indicate the beginning and end of the inbox. + + Leave this command running. + +1. Open a fourth terminal window, enter the Docker container with the command `docker exec -it octez-container /bin/sh`, and go to the `hello_world_kernel` folder. + +1. In this fourth terminal window, run this command to simulate adding a message to the smart rollup inbox: + + ```bash + octez-client send smart rollup message '[ "test" ]' from "bootstrap3" + ``` + +1. Go back to the third terminal window. + + This window shows the message that you sent in the fourth window, which it received in binary format, with the numbers representing the letters in "test." + + ``` + Got message: External([116, 101, 115, 116]) + ``` + +Now you can send messages to this rollup via Tezos layer 1 and act on them in the rollup code. + +## Next steps + +{% comment %} +Commenting this out because there's not enough info for a tutorial user to do this without further information; consider adding this because it would be good to be able to send messages (that is, call contracts) from the rollup, and I don't know how you'd do that in the sandbox. + +Currently, your rollup and kernel are running in sandbox mode. +If you want to explore further, you can try deploying the rollup to a testnet as you do in the [Deploy a smart contract](../deploy-your-first-smart-contract/) tutorial. +The workflow for deploying to a testnet is similar to the workflow that you used to deploy to the sandbox: + +1. Configure the network to use the testnet +1. Run a node (needs to synchronize with the network β€” can make use of [snapshots](https://tezos.gitlab.io/user/snapshots.html)) +1. Create or import an account and fun it by a faucet +1. Originate the rollup to the testnet +1. Start the rollup node +1. Check the log file +{% /comment %} + +To continue your work with smart rollups, you can you can explore examples from the [kernel gallery](https://gitlab.com/tezos/kernel-gallery/-/tree/main/) or create your own. + +## References + +- [Smart rollup documentation](https://tezos.gitlab.io/alpha/smart_rollups.html) +- [Smart rollup kernel SDK](https://gitlab.com/tezos/tezos/-/tree/master/src/kernel_sdk) +- [Smart rollup kernel examples](https://gitlab.com/tezos/kernel-gallery/-/tree/main/) +- [Tezos smart rollups resources](https://airtable.com/shrvwpb63rhHMiDg9/tbl2GNV1AZL4dkGgq) +- [Tezos testnets](https://teztnets.xyz/) +- [Originating the installer kernel](https://tezos.stackexchange.com/questions/4784/how-to-originating-a-smart-rollup-with-an-installer-kernel/5794#5794) +- [Docker documentation](https://docs.docker.com/get-started/) diff --git a/src/pages/tutorials/smart-rollups/set-up.md b/src/pages/tutorials/smart-rollups/set-up.md new file mode 100644 index 000000000..44237b7de --- /dev/null +++ b/src/pages/tutorials/smart-rollups/set-up.md @@ -0,0 +1,92 @@ +--- +id: set-up +title: "Part 1: Setting up the application" +lastUpdated: 25th October 2023 +--- + +To set up the application for the tutorial, you must configure Rust to build the kernel and start a Docker container that has resources that are needed to debug and deploy it. + +## Building the application + +Follow these steps to get the application code and build it: + +1. Clone the repository with the tutorial application: + + ```bash + git clone https://gitlab.com/trili/hello-world-kernel.git + cd hello-world-kernel/ + ``` + +1. Configure Rust to build WebAssembly applications: + + 1. Verify that you have Rust version 1.73.0 or later installed by running `rustc --version`. + + 1. If you have an earlier version of Rust, use the `rustup` command to use version 1.73.0: + + ```bash + rustup override set 1.73.0 + ``` + + 1. Add WASM as a compilation target for Rust by running this command: + + ```bash + rustup target add wasm32-unknown-unknown + ``` + +1. Build the application by running this command: + + ```bash + cargo build --target wasm32-unknown-unknown + ``` + + If the application builds correctly, the terminal shows a message that looks like "Finished dev [unoptimized + debuginfo] target(s) in 0.44s." + You can see the compiled application in the `target/wasm32-unknown-unknown/debug` folder. + In particular, the compiled kernel is in the `hello_world_kernel.wasm` file. + +Now the kernel is compiled into a single file that nodes can run. + +## Start the Docker container + +Tezos provides a Docker image that contains the Octez client, which allows you to interact with Tezos from the command line. +Later, you will use this image to run a sandbox Tezos environment for testing the rollup. + +1. Make sure that Docker desktop is running. + +1. Pull the most recent Tezos Docker image, which contains the most recent version of Octez: + + ```bash + docker pull tezos/tezos:master + ``` + + You can install Octez directly on your system, but keeping it in Docker is faster and more convenient for running the tutorial application. + +1. Make sure you are in the `hello-world-kernel` folder, at the same level as the `Cargo.toml` and `sandbox_node.sh` files. + +1. Run this command to start the Docker image, open a command-line terminal in that image, and mount the `hello-world-kernel` folder in it: + + ```bash + docker run -it --rm --volume $(pwd):/home/tezos/hello-world-kernel --entrypoint /bin/sh --name octez-container tezos/tezos:master + ``` + + Your command-line prompt changes to indicate that it is now inside the running Docker container. + This image includes the Octez command-line client and other Tezos tools. + It also uses the docker `--volume` argument to mount the contents of the `hello-world-kernel` folder in the container so you can use those files within the container. + +1. Verify that the container has the necessary tools by running these commands: + + ```bash + octez-node --version + octez-smart-rollup-wasm-debugger --version + octez-smart-rollup-node-alpha --version + octez-client --version + ``` + + Each of these commands should print a version number. + The specific version number is not important as long as you retrieved the latest image with the `docker pull tezos/tezos:master` command. + + Don't close this terminal window or exit the Docker terminal session, because Docker will close the container. + If you accidentally close the container, you can run the `docker run ...` command again to open a new one. + +Now the application is built and you have an environment that you can debug it in. +For the rest of the tutorial, you must be aware of whether you are running commands inside or outside of the Docker container. +The container has Octez but not Rust, so you run Rust commands outside of the container and Octez commands inside the container.