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