From 20f3de1ef5832237ace00ae788e43f763f15c6b3 Mon Sep 17 00:00:00 2001 From: Benjamin Fuentes Date: Mon, 25 Sep 2023 09:01:28 +0200 Subject: [PATCH 1/8] part 1 changed --- .../build-an-nft-marketplace/index.md | 295 ++++++++++-------- 1 file changed, 173 insertions(+), 122 deletions(-) diff --git a/src/pages/tutorials/build-an-nft-marketplace/index.md b/src/pages/tutorials/build-an-nft-marketplace/index.md index be87dde48..cb711d97a 100644 --- a/src/pages/tutorials/build-an-nft-marketplace/index.md +++ b/src/pages/tutorials/build-an-nft-marketplace/index.md @@ -1,7 +1,7 @@ --- id: build-an-nft-marketplace title: Build an NFT Marketplace -lastUpdated: 10th July 2023 +lastUpdated: 22nd September 2023 --- ## Introduction @@ -74,21 +74,22 @@ On a second time, we will import the token contract into the marketplace unique ## Prerequisites -#### Required +### 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 @@ -109,7 +110,7 @@ You will require to copy some code from this git repository later, so you can cl ```bash taq init training cd training -taq install @taqueria/plugin-ligo@next +taq install @taqueria/plugin-ligo ``` {% callout type="warning" %} @@ -135,7 +136,7 @@ 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" +TAQ_LIGO_IMAGE=ligolang/ligo:0.73.0 taq ligo --command "install @ligo/fa" ``` ### NFT marketplace contract @@ -170,27 +171,10 @@ type storage = metadata: NFT.Metadata.t, token_metadata: NFT.TokenMetadata.t, operators: NFT.Operators.t, - token_ids : set + token_ids : set }; 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: @@ -201,40 +185,89 @@ Explanations: - `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 + - `set` : cache for keys of token_id bigmap - `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 +The contract compiles, now let's write `transfer,balance_of,update_operators` entrypoints. We will do a passthrough call to the underlying library. -```bash -TAQ_LIGO_IMAGE=ligolang/ligo:0.60.0 taq compile nft.jsligo -``` - -{% 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: NFT.transfer, s: storage): ret => { + 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 + } + ] +}; -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: NFT.balance_of, s: storage): ret => { + 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 + } + ] +}; -```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: NFT.update_operators, s: storage): ret => { + 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 + } + ] +}; ``` Explanations: @@ -246,57 +279,45 @@ Explanations: 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 => { - - 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(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], - }); +@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"]`)], + ["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 + NFT.Ledger.t, + 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) + } + ] +}; ``` Explanations: @@ -312,24 +333,50 @@ We have finished the smart contract implementation for this first training, let' 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" +#import "nft.jsligo" "Contract" +#import "@ligo/fa/lib/fa2/nft/NFT.jsligo" "NFT" 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 - }; + { + administrators: Set.literal( + list(["tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb" as address]) + ) as set
, + ledger: Big_map.empty as NFT.Ledger.t, + 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 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 + }; ``` -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:0.73.0 taq compile nft.jsligo +taq install @taqueria/plugin-taquito taq deploy nft.tz -e "testing" ``` @@ -362,7 +409,7 @@ taq deploy nft.tz -e "testing" ┌──────────┬──────────────────────────────────────┬───────┬──────────────────┬────────────────────────────────┐ │ Contract │ Address │ Alias │ Balance In Mutez │ Destination │ ├──────────┼──────────────────────────────────────┼───────┼──────────────────┼────────────────────────────────┤ -│ nft.tz │ KT1PLo2zWETRkmqUFEiGqQNVUPorWHVHgHMi │ nft │ 0 │ https://ghostnet.ecadinfra.com │ +│ nft.tz │ KT1SdFLhhL4Z4n4hWoMPxpa1R5LAq25TwQFi │ nft │ 0 │ https://ghostnet.ecadinfra.com │ └──────────┴──────────────────────────────────────┴───────┴──────────────────┴────────────────────────────────┘ ``` @@ -389,15 +436,15 @@ 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",` +> ` "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: @@ -675,11 +722,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( @@ -877,6 +924,8 @@ import { } from "@mui/icons-material"; ``` +and some variables + ```typescript const [activeStep, setActiveStep] = React.useState(0); @@ -921,4 +970,6 @@ Now you can see all NFTs 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). From a779ebdc61592408ccc0a7756e7eca0490445e06 Mon Sep 17 00:00:00 2001 From: Benjamin Fuentes Date: Mon, 25 Sep 2023 09:19:06 +0200 Subject: [PATCH 2/8] part 2 update --- .../build-an-nft-marketplace/part-2.md | 406 ++++-------------- 1 file changed, 77 insertions(+), 329 deletions(-) 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..60467efb3 100644 --- a/src/pages/tutorials/build-an-nft-marketplace/part-2.md +++ b/src/pages/tutorials/build-an-nft-marketplace/part-2.md @@ -41,136 +41,27 @@ type storage = metadata: NFT.Metadata.t, token_metadata: NFT.TokenMetadata.t, operators: NFT.Operators.t, - token_ids : set + token_ids : set }; ``` -Add 2 variants `Buy` and `Sell` to the parameter - -```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 - } - ] - } - } - ); -``` - 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 also the initial storage on file `nft.storageList.jsligo` to initialize `offers` ```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 - }; +... + offers: Map.empty as map, +... ``` Finally, compile the contract ```bash -TAQ_LIGO_IMAGE=ligolang/ligo:0.64.2 taq compile nft.jsligo +TAQ_LIGO_IMAGE=ligolang/ligo:0.73.0 taq compile nft.jsligo ``` ### Sell at an offer price @@ -178,115 +69,47 @@ 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 = + 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"); //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 = + NFT.Operators.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: @@ -294,7 +117,6 @@ 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` ### Buy a bottle on the marketplace @@ -303,124 +125,51 @@ Now that we have 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, + return match( + Map.find_opt(token_id, s.offers), { - 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 - } + 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") ); - 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 = + NFT.Ledger.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 + offers: Map.update(token_id, None(), s.offers), + ledger: ledger } ] } } - ); + ) +}; ``` Explanation: @@ -428,14 +177,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. ```bash -TAQ_LIGO_IMAGE=ligolang/ligo:0.64.2 taq compile nft.jsligo +TAQ_LIGO_IMAGE=ligolang/ligo:0.73.0 taq compile nft.jsligo taq deploy nft.tz -e "testing" ``` @@ -443,11 +191,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 │ KT1WZFHYKPpfjPKMsCqLRQJzSUSrBWAm3gKC │ nft │ 0 │ https://ghostnet.ecadinfra.com │ └──────────┴──────────────────────────────────────┴───────┴──────────────────┴────────────────────────────────┘ ``` -** We have implemented and deployed the smart contract (backend)!** +**We have implemented and deployed the smart contract (backend)!** ## NFT Marketplace front @@ -457,7 +205,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 @@ -1067,7 +815,7 @@ As you are connected with the default administrator you can see your own unique - Click on `bottle offers` sub menu - You are now the owner of this bottle, you can resell it at your own price, etc ... -## Conclusion +## Conclusion You created an NFT collection marketplace from the Ligo library, now you can buy and sell NFTs at your own price. From 9bdba3995a28a1978898167e89880593150d6cee Mon Sep 17 00:00:00 2001 From: Benjamin Fuentes Date: Mon, 25 Sep 2023 09:30:14 +0200 Subject: [PATCH 3/8] part 3 update --- .../build-an-nft-marketplace/part-3.md | 290 ++++++++++-------- 1 file changed, 164 insertions(+), 126 deletions(-) diff --git a/src/pages/tutorials/build-an-nft-marketplace/part-3.md b/src/pages/tutorials/build-an-nft-marketplace/part-3.md index d1840e873..b19851b7a 100644 --- a/src/pages/tutorials/build-an-nft-marketplace/part-3.md +++ b/src/pages/tutorials/build-an-nft-marketplace/part-3.md @@ -27,7 +27,7 @@ cd .. Point to the new template changing the first import line of your `nft.jsligo` file to ```ligolang -#import "@ligo/fa/lib/fa2/asset/single_asset.mligo" "SINGLEASSET" +#import "@ligo/fa/lib/fa2/asset/single_asset.jsligo" "SINGLEASSET" ``` It means you will change the namespace from `NFT` to `SINGLEASSET` everywhere (like this you are sure to use the correct library) @@ -61,159 +61,197 @@ Explanation: - 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]; -``` +- Replace all `token_ids` fields by `owners` field on the file `nft.jsligo` 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"]`)], + ["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 + SINGLEASSET.Ledger.t, + 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) + } + ] +}; ``` Edit the `sell` function to replace `token_id` by `quantity`, we 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 = + 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"); //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 = + SINGLEASSET.Operators.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")); - //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); + return match( + Map.find_opt(seller, s.offers), + { + None: () => failwith("3"), + Some: (offer: offer) => { + //check if quantity is enough - //update new offer - const newOffer = {...offer,quantity : abs(offer.quantity - quantity)}; + if (quantity > offer.quantity) return failwith("4"); + //check if amount have been paid enough - return [list([op]) as list, {...s, offers : Map.update(seller,Some(newOffer),s.offers), ledger : ledger, owners : Set.add(Tezos.get_source(),s.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 = + SINGLEASSET.Ledger.decrease_token_amount_for_user(s.ledger)(seller)( + quantity + ); + ledger = + SINGLEASSET.Ledger.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, + 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}]; - } - }); -``` - 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" +#import "nft.jsligo" "Contract" +#import "@ligo/fa/lib/fa2/asset/single_asset.jsligo" "SINGLEASSET" const default_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, - } -; + { + 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.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 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 + }; + ``` Compile again and deploy to ghostnet. ```bash -TAQ_LIGO_IMAGE=ligolang/ligo:0.64.2 taq compile nft.jsligo +TAQ_LIGO_IMAGE=ligolang/ligo:0.73.0 taq compile nft.jsligo taq deploy nft.tz -e "testing" ``` @@ -221,13 +259,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 │ KT1QAV6tJ4ZVSDSF6WqCr4qRD7a33DY3iDpj │ nft │ 0 │ https://ghostnet.ecadinfra.com │ └──────────┴──────────────────────────────────────┴───────┴──────────────────┴────────────────────────────────┘ ``` We finished the smart contract! _(backend)_ -# NFT Marketplace front +## NFT Marketplace front Generate Typescript classes and go to the frontend to run the server @@ -235,10 +273,10 @@ 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. Replace the function `refreshUserContextOnPageReload` by @@ -392,11 +430,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( @@ -1169,7 +1207,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 )} @@ -1181,7 +1219,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 @@ -1349,7 +1387,7 @@ For buying, ![buy.png](/images/buy_part3.png) -## Conclusion +## Conclusion You are now able to play with a unique NFT collection from the Ligo library. From e93b0c963a33c20195bcd1f4eacf99b8c4fdf2f6 Mon Sep 17 00:00:00 2001 From: Benjamin Fuentes Date: Mon, 25 Sep 2023 09:36:11 +0200 Subject: [PATCH 4/8] part 4 update --- .../build-an-nft-marketplace/part-4.md | 382 ++++++++---------- 1 file changed, 169 insertions(+), 213 deletions(-) diff --git a/src/pages/tutorials/build-an-nft-marketplace/part-4.md b/src/pages/tutorials/build-an-nft-marketplace/part-4.md index bf254f935..b94ec1456 100644 --- a/src/pages/tutorials/build-an-nft-marketplace/part-4.md +++ b/src/pages/tutorials/build-an-nft-marketplace/part-4.md @@ -40,259 +40,215 @@ Change the `storage` definition ```ligolang type offer = { - quantity : nat, - price : nat + quantity: nat, + price: nat }; -type storage = - { - administrators: set
, - 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 - }; -``` - -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]; +type storage = { + administrators: set
, + 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.owner, MULTIASSET.token_id]>, + token_ids: set +}; ``` 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"]`)], + ["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 MULTIASSET.Ledger.t, + 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) + } + ] +}; ``` 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 = + MULTIASSET.Ledger.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 = + MULTIASSET.Operators.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 [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) }]; - } - }); -}; -``` + return match( + Map.find_opt([seller, token_id], s.offers), + { + None: () => failwith("3"), + Some: (offer: offer) => { + //check if amount have been paid enough -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 = + MULTIASSET.Ledger.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 = + 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 [ - 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, + owner_token_ids: Set.add( + [Tezos.get_source(), token_id], + s.owner_token_ids + ) } ] } } - ); + ) +}; ``` +On `transfer,balance_of and update_ops` functions, change : + +- `owners: s.owners` by `owner_token_ids: s.owner_token_ids,token_ids: s.token_ids` +- `owners: ret2[1].owners` by `owner_token_ids: ret2[1].owner_token_ids,token_ids: ret2[1].token_ids` + Change the initial storage to ```ligolang -#include "nft.jsligo" +#import "nft.jsligo" "Contract" +#import "@ligo/fa/lib/fa2/asset/multi_asset.jsligo" "MULTIASSET" 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 - } -; + { + administrators: Set.literal( + list(["tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb" as address]) + ) as set
, + offers: Map.empty as map<[address, nat], Contract.offer>, + ledger: Big_map.empty as MULTIASSET.Ledger.t, + 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 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.owner, MULTIASSET.token_id]>, + token_ids: Set.empty as set + }; + ``` Compile again and deploy to ghostnet ```bash -TAQ_LIGO_IMAGE=ligolang/ligo:0.64.2 taq compile nft.jsligo +TAQ_LIGO_IMAGE=ligolang/ligo:0.73.0 taq compile nft.jsligo taq deploy nft.tz -e "testing" ``` @@ -300,11 +256,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 │ KT1LwiszjMiEXasgtuHLswaMjUUdm5ARBmvk │ nft │ 0 │ https://ghostnet.ecadinfra.com │ └──────────┴──────────────────────────────────────┴───────┴──────────────────┴────────────────────────────────┘ ``` -** Hooray! We have finished the smart contract _(backend)_ ** +**Hooray! We have finished the smart contract _(backend)_** ## NFT Marketplace front @@ -314,7 +270,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 ``` ## Update in `App.tsx` @@ -477,11 +433,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( @@ -1297,7 +1253,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 +1264,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 @@ -1487,7 +1443,7 @@ For buying, To add more collections, go to the Mint page and repeat the process. -# Conclusion +## Conclusion You are able to use any NFT template from the Ligo library. From d08c816d0d7c476f8ed9ff2667e94491f9c42840 Mon Sep 17 00:00:00 2001 From: Benjamin Fuentes Date: Thu, 12 Oct 2023 13:47:20 +0200 Subject: [PATCH 5/8] Merging comments/changes of Yuxin into the branch --- .../build-an-nft-marketplace/index.md | 324 ++++++++++-------- 1 file changed, 173 insertions(+), 151 deletions(-) diff --git a/src/pages/tutorials/build-an-nft-marketplace/index.md b/src/pages/tutorials/build-an-nft-marketplace/index.md index cb711d97a..d9a8413a9 100644 --- a/src/pages/tutorials/build-an-nft-marketplace/index.md +++ b/src/pages/tutorials/build-an-nft-marketplace/index.md @@ -1,62 +1,48 @@ --- id: build-an-nft-marketplace -title: Build an NFT Marketplace -lastUpdated: 22nd September 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. - -Here are different categorizations of assets. - -![](http://jingculturecommerce.com/wp-content/uploads/2021/03/nft-assets-1024x614.jpg) - -## Wine marketplace +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. -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. +In the first part, you will learn: -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 +- 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" %} +{% 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 | +To begin with, we will introduce you to the critical concepts of FA, IPFS, and smart contracts used for the marketplace. -{% 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 %} +### What is FA? + +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. -## 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. -## Smart Contracts +### Smart Contracts Overview We will use 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: @@ -64,16 +50,43 @@ On Tezos, FA2 is the standard for Non-Fungible Token contracts. We will be using - 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, we will import the token contract into the marketplace unique contract. The latter will bring 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 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 %} + ## Prerequisites +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 @@ -92,20 +105,22 @@ On a second time, we will import the token contract into the marketplace unique - [taqueria VS Code extension](https://marketplace.visualstudio.com/items?itemName=ecadlabs.taqueria-vscode): visualize your project and execute tasks - -## Smart contract +## Smart Contract Modification 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" %} +{% 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, we will set up our smart contract structure. ```bash taq init training @@ -113,19 +128,11 @@ cd training taq install @taqueria/plugin-ligo ``` -{% 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 -``` - **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, we will 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) @@ -136,12 +143,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.73.0 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, we will create the NFT marketplace contract with `taqueria` ```bash taq create contract nft.jsligo @@ -150,7 +158,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([ @@ -164,45 +172,41 @@ 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]; ``` 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. +- the first line `#import "@ligo/fa/lib/fa2/nft/nft.impl.jsligo" "FA2Impl"` imports the Ligo FA library implmentation 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 - - `set` : cache for keys of token_id bigmap + - `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` The contract compiles, now let's write `transfer,balance_of,update_operators` entrypoints. We will do a passthrough call to the underlying library. ```ligolang @entry -const transfer = (p: NFT.transfer, s: storage): ret => { - const ret2: [list, NFT.storage] = - NFT.transfer([ +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, - token_ids: s.token_ids - }] + } ); return [ ret2[0], @@ -212,23 +216,21 @@ const transfer = (p: NFT.transfer, s: storage): ret => { metadata: ret2[1].metadata, token_metadata: ret2[1].token_metadata, operators: ret2[1].operators, - token_ids: ret2[1].token_ids } ] }; @entry -const balance_of = (p: NFT.balance_of, s: storage): ret => { - const ret2: [list, NFT.storage] = - NFT.balance_of([ +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, - token_ids: s.token_ids - }] + } ); return [ ret2[0], @@ -238,23 +240,21 @@ const balance_of = (p: NFT.balance_of, s: storage): ret => { 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: NFT.update_operators, s: storage): ret => { - const ret2: [list, NFT.storage] = - NFT.update_ops([ +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, - token_ids: s.token_ids - }] + } ); return [ ret2[0], @@ -264,15 +264,14 @@ const update_operators = (p: NFT.update_operators, s: storage): ret => { metadata: ret2[1].metadata, token_metadata: ret2[1].token_metadata, operators: ret2[1].operators, - token_ids: ret2[1].token_ids } ] }; ``` -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 +- every `FA2Impl.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 {% callout type="note" %} @@ -284,11 +283,16 @@ Let's add the `Mint` function now. Add the new function ```ligolang @entry const mint = ( - [token_id, name, description, symbol, ipfsUrl] - : [nat, bytes, bytes, bytes, bytes], + [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"); + if (! Set.mem(Tezos.get_sender(), s.administrators)) return failwith("1"); const token_info: map = Map.literal( list( @@ -307,48 +311,52 @@ const mint = ( { ...s, ledger: Big_map.add(token_id, Tezos.get_sender(), s.ledger) as - NFT.Ledger.t, + 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 NFT.Operators.t, - token_ids: Set.add(token_id, s.token_ids) + operators: Big_map.empty as FA2Impl.NFT.operators, } ] }; ``` -Explanations: +Explanation: - `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 +- if you want to know the `size of the NFT collection`, we will require an indexer on the frontend side. It is not possible to have this information on the contract (because we deal with a big_map that has not a .keys() function returning the keys) unless you add and additional element on the storage to cache it 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`) +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" -#import "@ligo/fa/lib/fa2/nft/NFT.jsligo" "NFT" -const default_storage = - { - administrators: Set.literal( - list(["tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb" as address]) - ) as set
, - ledger: Big_map.empty as NFT.Ledger.t, - metadata: Big_map.literal( - list( + +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`], [ - ["", bytes `tezos-storage:data`], - [ - "data", - bytes - `{ + "data", + bytes + `{ "name":"FA2 NFT Marketplace", "description":"Example of FA2 implementation", "version":"0.0.1", @@ -362,20 +370,19 @@ const default_storage = "errors": [], "views": [] }` - ] ] - ) - ) 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 and deploy to ghostnet ```bash -TAQ_LIGO_IMAGE=ligolang/ligo:0.73.0 taq compile nft.jsligo +TAQ_LIGO_IMAGE=ligolang/ligo:1.0.0 taq compile nft.jsligo taq install @taqueria/plugin-taquito taq deploy nft.tz -e "testing" ``` @@ -409,15 +416,17 @@ taq deploy nft.tz -e "testing" ┌──────────┬──────────────────────────────────────┬───────┬──────────────────┬────────────────────────────────┐ │ Contract │ Address │ Alias │ Balance In Mutez │ Destination │ ├──────────┼──────────────────────────────────────┼───────┼──────────────────┼────────────────────────────────┤ -│ nft.tz │ KT1SdFLhhL4Z4n4hWoMPxpa1R5LAq25TwQFi │ nft │ 0 │ https://ghostnet.ecadinfra.com │ +│ nft.tz │ KT18sgGX5nu4BzwV2JtpQy4KCqc8cZU5MwnN │ nft │ 0 │ https://ghostnet.ecadinfra.com │ └──────────┴──────────────────────────────────────┴───────┴──────────────────┴────────────────────────────────┘ ``` -** We have finished the backend! ** +**We have finished the backend!** ## NFT Marketplace frontend -## Get the react boilerplate +This section guides you step-by-step in setting up an intuitive frontend. + +### Step 1: Get the react boilerplate To save time, we have a [boilerplate ready for the UI](https://github.com/marigold-dev/training-nft-1/tree/main/reactboilerplateapp) @@ -456,13 +465,13 @@ If you try to connect you are redirected to `/` path that is also the wine catal There are no bottle collections yet, so we need to create the mint page. -## Mint Page +### Step 2: Mint Page 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 @@ -619,7 +628,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({ @@ -674,7 +685,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"; @@ -697,9 +708,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"; @@ -709,6 +720,8 @@ import { char2Bytes } from "@taquito/utils"; import { TransactionInvalidBeaconError } from "./TransactionInvalidBeaconError"; ``` +Add the `mint` function inside your `MintPage` Component function + ```typescript const { enqueueSnackbar } = useSnackbar(); @@ -782,6 +795,8 @@ const mint = async (newTokenDefinition: TZIP21TokenMetadata) => { }; ``` +> Note : organize/fix duplicated import declarations if necessary + ![mint form](/images/mintForm.png) Explanations: @@ -794,16 +809,16 @@ Explanations: > 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. -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 @@ -897,7 +912,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"; @@ -922,9 +937,20 @@ 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 +and some variables inside your `MintPage` Component function ```typescript const [activeStep, setActiveStep] = React.useState(0); @@ -944,17 +970,13 @@ 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) @@ -966,7 +988,7 @@ 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. From de9acf81ecebc9258bd03dd1757304b9facc3c99 Mon Sep 17 00:00:00 2001 From: Benjamin Fuentes Date: Thu, 12 Oct 2023 17:25:13 +0200 Subject: [PATCH 6/8] fixes after Tim's comments --- .../build-an-nft-marketplace/index.md | 85 ++++++++++--------- 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/src/pages/tutorials/build-an-nft-marketplace/index.md b/src/pages/tutorials/build-an-nft-marketplace/index.md index d9a8413a9..472f597c3 100644 --- a/src/pages/tutorials/build-an-nft-marketplace/index.md +++ b/src/pages/tutorials/build-an-nft-marketplace/index.md @@ -8,13 +8,14 @@ lastUpdated: 11th October 2023 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. -In the first part, you will learn: +In the first part, you learn: - The concepts of FA, IPFS, and smart contracts. - How to build an NFT Marketplace from the ligo/fa library. {% 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. +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) - [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 @@ -24,11 +25,9 @@ Here we present Part 1 of 4 of a training course by [Marigold](https://www.marig ## Key Concepts -To begin with, we will introduce you to the critical concepts of FA, IPFS, and smart contracts used for the marketplace. - ### What is FA? -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. +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. Here are different categorizations of assets. @@ -36,15 +35,15 @@ Here are different categorizations of assets. ### 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 Overview -We will use two contracts for the marketplace. +There are two contracts for the marketplace. #### 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 @@ -52,7 +51,7 @@ On Tezos, FA2 is the standard for Non-Fungible Token contracts. We will be using #### 2. Marketplace unique contract -Next, 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 @@ -80,7 +79,7 @@ You can play with the [final demo](https://demo.winefactory.marigold.dev/). | multi asset | 0..n | 1..n | {% 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 +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 @@ -107,10 +106,10 @@ Before building an NFT marketplace, ensure you have the following tools on hand. ## Smart Contract Modification -We will use `taqueria` to shape the project structure, then create the NFT marketplace smart contract thanks to the `ligo/fa` library. +Use **Taqueria** to shape the project structure, then create the NFT marketplace smart contract thanks to the `ligo/fa` library. {% callout type="note" %} -You will require to copy some code from this git repository later, so you can clone it with: +You 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 @@ -120,7 +119,7 @@ git clone https://github.com/marigold-dev/training-nft-1.git ### Step 1: Taq'ify your project -First, we will set up our smart contract structure. +First, set up our smart contract structure. ```bash taq init training @@ -132,7 +131,7 @@ taq install @taqueria/plugin-ligo ### Step 2: FA2 contract -Next, we will 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. +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) @@ -149,7 +148,7 @@ TAQ_LIGO_IMAGE=ligolang/ligo:1.0.0 taq ligo --command "install @ligo/fa" ### Step 3: NFT marketplace contract -Then, we will create the NFT marketplace contract with `taqueria` +Then, create the NFT marketplace contract with `taqueria` ```bash taq create contract nft.jsligo @@ -185,15 +184,15 @@ type ret = [list, storage]; Explanations: -- the first line `#import "@ligo/fa/lib/fa2/nft/nft.impl.jsligo" "FA2Impl"` imports the Ligo FA library implmentation 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 +- 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` -The contract compiles, now let's write `transfer,balance_of,update_operators` entrypoints. We will do a passthrough call to the underlying library. +The contract compiles, now let's write `transfer,balance_of,update_operators` entrypoints. You do a passthrough call to the underlying library. ```ligolang @entry @@ -271,11 +270,11 @@ const update_operators = (p: FA2Impl.TZIP12.update_operators, s: storage): ret = Explanation: -- every `FA2Impl.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 @@ -300,6 +299,8 @@ const mint = ( ["name", name], ["description", description], ["interfaces", (bytes `["TZIP-12"]`)], + ["artifactUri", ipfsUrl], + ["displayUri", ipfsUrl], ["thumbnailUri", ipfsUrl], ["symbol", symbol], ["decimals", (bytes `0`)] @@ -325,13 +326,13 @@ const mint = ( Explanation: -- `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. +- `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 we don't need to set a total supply on each NFT. -- if you want to know the `size of the NFT collection`, we will require an indexer on the frontend side. It is not possible to have this information on the contract (because we deal with a big_map that has not a .keys() function returning the keys) unless you add and additional element on the storage to cache it +- 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 -We have finished the smart contract implementation for this first training, let's prepare the deployment to ghostnet. +Smart contract implementation for this first training is finished, let's prepare the deployment to ghostnet. Compile the file to create a default taqueria initial storage and parameter file @@ -388,7 +389,7 @@ 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: @@ -420,7 +421,7 @@ taq deploy nft.tz -e "testing" └──────────┴──────────────────────────────────────┴───────┴──────────────────┴────────────────────────────────┘ ``` -**We have finished the backend!** +**Backend is finished!** ## NFT Marketplace frontend @@ -428,7 +429,7 @@ This section guides you step-by-step in setting up an intuitive frontend. ### Step 1: Get the react boilerplate -To save time, we have a [boilerplate ready for the UI](https://github.com/marigold-dev/training-nft-1/tree/main/reactboilerplateapp) +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`) @@ -452,26 +453,26 @@ yarn install yarn dev ``` -> Note : On `Mac` :green_apple:, `sed` does not work as Unix, change the start script on package.json to +> 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. ### 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 -In `MintPage.tsx`, replace the `HTML` template starting with `` with this one : +In `MintPage.tsx`, replace the **HTML** template starting with `` with this one : ```html @@ -801,13 +802,13 @@ const mint = async (newTokenDefinition: TZIP21TokenMetadata) => { 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 inside your `MintPage` Component function , every time you have a new token minted, you increment the counter for the next one @@ -970,19 +971,19 @@ const handleStepChange = (step: number) => { ## Let's play -- 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. +- 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 **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 @@ -992,6 +993,6 @@ Now you can see all NFTs 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. +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). From 46b0f68e2a542cafa3ab00fd95cadb3fcb2b6619 Mon Sep 17 00:00:00 2001 From: Benjamin Fuentes Date: Fri, 13 Oct 2023 14:59:27 +0200 Subject: [PATCH 7/8] part 2 , 3 and 4 updated with last Ligo V1 and library ligo/fa v1.0.8 --- .../build-an-nft-marketplace/part-2.md | 215 +++++++++------ .../build-an-nft-marketplace/part-3.md | 219 ++++++++------- .../build-an-nft-marketplace/part-4.md | 258 +++++++++--------- 3 files changed, 375 insertions(+), 317 deletions(-) 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 60467efb3..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,25 +28,23 @@ 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 +Add `offers` field to storage, it should look like this below : ```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 - }; +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: @@ -50,18 +52,51 @@ Explanation: - an `offer` is an NFT _(owned by someone)_ with a price - `storage` has a new field to store `offers`: a `map` of offers -Update also the initial storage on file `nft.storageList.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 -... - offers: Map.empty as map, -... +#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.73.0 taq compile nft.jsligo +TAQ_LIGO_IMAGE=ligolang/ligo:1.0.0 taq compile nft.jsligo ``` ### Sell at an offer price @@ -74,22 +109,20 @@ const sell = ([token_id, price]: [nat, nat], s: storage): ret => { //check balance of seller const sellerBalance = - NFT.Storage.get_balance( + FA2Impl.NFT.get_balance( + [Tezos.get_source(), token_id], { 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"); //need to allow the contract itself to be an operator on behalf of the seller const newOperators = - NFT.Operators.add_operator( + FA2Impl.Sidecar.add_operator( s.operators, Tezos.get_source(), Tezos.get_self_address(), @@ -116,11 +149,11 @@ 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 +- `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 @@ -129,11 +162,11 @@ Edit the smart contract to add the `buy` feature 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) => { + 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( @@ -150,7 +183,7 @@ const buy = ([token_id, seller]: [nat, address], s: storage): ret => { //transfer tokens from seller to buyer const ledger = - NFT.Ledger.transfer_token_from_user_to_user( + FA2Impl.Sidecar.transfer_token_from_user_to_user( s.ledger, token_id, seller, @@ -161,14 +194,11 @@ const buy = ([token_id, seller]: [nat, address], s: storage): ret => { return [ list([op]) as list, { - ...s, - offers: Map.update(token_id, None(), s.offers), - ledger: ledger + ...s, offers: Map.update(token_id, None(), s.offers), ledger: ledger } ] } - } - ) + } }; ``` @@ -180,10 +210,10 @@ Explanation: ### 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.73.0 taq compile nft.jsligo +TAQ_LIGO_IMAGE=ligolang/ligo:1.0.0 taq compile nft.jsligo taq deploy nft.tz -e "testing" ``` @@ -191,11 +221,11 @@ taq deploy nft.tz -e "testing" ┌──────────┬──────────────────────────────────────┬───────┬──────────────────┬────────────────────────────────┐ │ Contract │ Address │ Alias │ Balance In Mutez │ Destination │ ├──────────┼──────────────────────────────────────┼───────┼──────────────────┼────────────────────────────────┤ -│ nft.tz │ KT1WZFHYKPpfjPKMsCqLRQJzSUSrBWAm3gKC │ 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 @@ -218,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, @@ -260,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, @@ -299,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 ); @@ -419,9 +466,8 @@ export default function OffersPage() { {"Description : " + - nftContratTokenMetadataMap.get( - token_id.toNumber() - )?.description} + nftContratTokenMetadataMap.get(token_id) + ?.description} } @@ -429,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); }} > @@ -529,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 @@ -564,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 @@ -721,7 +757,7 @@ export default function WineCataloguePage() { {"Description : " + nftContratTokenMetadataMap.get( - token_id.toNumber() + token_id.toString() )?.description} @@ -734,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` - -- Replace all `token_ids` fields by `owners` field on the file `nft.jsligo` Edit the `mint` function to add the `quantity` extra param, and finally change the `return` ```ligolang @entry const mint = ( - [quantity, name, description, symbol, ipfsUrl] - : [nat, bytes, bytes, bytes, bytes], + [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"); + if (! Set.mem(Tezos.get_sender(), s.administrators)) return failwith("1"); const token_info: map = Map.literal( list( @@ -81,6 +82,8 @@ const mint = ( ["name", name], ["description", description], ["interfaces", (bytes `["TZIP-12"]`)], + ["artifactUri", ipfsUrl], + ["displayUri", ipfsUrl], ["thumbnailUri", ipfsUrl], ["symbol", symbol], ["decimals", (bytes `0`)] @@ -93,20 +96,19 @@ const mint = ( ...s, totalSupply: quantity, ledger: Big_map.literal(list([[Tezos.get_sender(), quantity as nat]])) as - SINGLEASSET.Ledger.t, + 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 SINGLEASSET.Operators.t, - owners: Set.add(Tezos.get_sender(), s.owners) + 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 @entry @@ -114,20 +116,19 @@ const sell = ([quantity, price]: [nat, nat], s: storage): ret => { //check balance of seller const sellerBalance = - SINGLEASSET.Storage.get_amount_for_owner( + FA2Impl.Sidecar.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"); //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())( + FA2Impl.Sidecar.add_operator(s.operators)(Tezos.get_source())( Tezos.get_self_address() ); //DECISION CHOICE: if offer already exists, we just override it @@ -154,11 +155,11 @@ Also edit the `buy` function to replace `token_id` by `quantity`, check quantiti 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) => { + return match(Map.find_opt(seller, s.offers)) { + when (None()): + failwith("3") + when (Some(offer)): + do { //check if quantity is enough if (quantity > offer.quantity) return failwith("4"); @@ -178,11 +179,11 @@ const buy = ([quantity, seller]: [nat, address], s: storage): ret => { //transfer tokens from seller to buyer let ledger = - SINGLEASSET.Ledger.decrease_token_amount_for_user(s.ledger)(seller)( + FA2Impl.Sidecar.decrease_token_amount_for_user(s.ledger)(seller)( quantity ); - ledger = - SINGLEASSET.Ledger.increase_token_amount_for_user(ledger)( + ledger + = FA2Impl.Sidecar.increase_token_amount_for_user(ledger)( Tezos.get_source() )(quantity); //update new offer @@ -194,12 +195,10 @@ const buy = ([quantity, seller]: [nat, address], s: storage): ret => { ...s, offers: Map.update(seller, Some(newOffer), s.offers), ledger: ledger, - owners: Set.add(Tezos.get_source(), s.owners) } ] } - } - ) + } }; ``` @@ -207,23 +206,22 @@ Edit the storage file `nft.storageList.jsligo` as it. (:warning: you can change ```ligolang #import "nft.jsligo" "Contract" -#import "@ligo/fa/lib/fa2/asset/single_asset.jsligo" "SINGLEASSET" -const default_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.literal( - list( + +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 Contract.FA2Impl.SingleAsset.ledger, + metadata: Big_map.literal( + list( + [ + ["", bytes `tezos-storage:data`], [ - ["", bytes `tezos-storage:data`], - [ - "data", - bytes - `{ + "data", + bytes + `{ "name":"FA2 NFT Marketplace", "description":"Example of FA2 implementation", "version":"0.0.1", @@ -237,21 +235,19 @@ const default_storage = "errors": [], "views": [] }` - ] ] - ) - ) 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 - }; - + ] + ) + ) 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.73.0 taq compile nft.jsligo +TAQ_LIGO_IMAGE=ligolang/ligo:1.0.0 taq compile nft.jsligo taq deploy nft.tz -e "testing" ``` @@ -259,11 +255,11 @@ taq deploy nft.tz -e "testing" ┌──────────┬──────────────────────────────────────┬───────┬──────────────────┬────────────────────────────────┐ │ Contract │ Address │ Alias │ Balance In Mutez │ Destination │ ├──────────┼──────────────────────────────────────┼───────┼──────────────────┼────────────────────────────────┤ -│ nft.tz │ KT1QAV6tJ4ZVSDSF6WqCr4qRD7a33DY3iDpj │ 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 @@ -278,7 +274,7 @@ yarn dev ### Update in `App.tsx` -We just need to fetch the token_id == 0. +Fetch the `token_id == 0`. Replace the function `refreshUserContextOnPageReload` by ```typescript @@ -298,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) { @@ -325,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"; @@ -759,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, @@ -811,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); @@ -847,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 ); @@ -862,7 +870,7 @@ export default function OffersPage() { console.log( "found for " + - owner + + ownerKey.key + " on token_id " + 0 + " with balance " + @@ -955,7 +963,7 @@ export default function OffersPage() { {"ID : " + 0} {"Description : " + - nftContratTokenMetadataMap.get(0)?.description} + nftContratTokenMetadataMap.get("0")?.description} } @@ -963,14 +971,14 @@ export default function OffersPage() { } - title={nftContratTokenMetadataMap.get(0)?.name} + title={nftContratTokenMetadataMap.get("0")?.name} /> {"ID : " + 0} {"Description : " + - nftContratTokenMetadataMap.get(0)?.description} + nftContratTokenMetadataMap.get("0") + ?.description} {"Seller : " + owner} @@ -1244,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.owner, MULTIASSET.token_id]>, - token_ids: set + + ledger: FA2Impl.Datatypes.ledger, + metadata: FA2Impl.TZIP16.metadata, + token_metadata: FA2Impl.TZIP12.tokenMetadata, + operators: FA2Impl.Datatypes.operators, }; ``` @@ -61,12 +64,18 @@ Update `mint` function ```ligolang @entry const mint = ( - [token_id, quantity, name, description, symbol, ipfsUrl] - : [nat, nat, bytes, bytes, bytes, bytes], + [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"); + if (! Set.mem(Tezos.get_sender(), s.administrators)) return failwith("1"); const token_info: map = Map.literal( list( @@ -74,6 +83,8 @@ const mint = ( ["name", name], ["description", description], ["interfaces", (bytes `["TZIP-12"]`)], + ["artifactUri", ipfsUrl], + ["displayUri", ipfsUrl], ["thumbnailUri", ipfsUrl], ["symbol", symbol], ["decimals", (bytes `0`)] @@ -88,18 +99,13 @@ const mint = ( [Tezos.get_sender(), token_id], quantity as nat, s.ledger - ) as MULTIASSET.Ledger.t, + ) 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 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) + operators: Big_map.empty as FA2Impl.Datatypes.operators } ] }; @@ -113,12 +119,12 @@ 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]); + 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( + 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 @@ -145,11 +151,11 @@ Same for the `buy` function 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) => { + 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 if (Tezos.get_amount() < offer.price * (1 as mutez)) return failwith( @@ -166,11 +172,11 @@ const buy = ([token_id, quantity, seller]: [nat, nat, address], s: storage): ret //transfer tokens from seller to buyer let ledger = - MULTIASSET.Ledger.decrease_token_amount_for_user( + FA2Impl.Sidecar.decrease_token_amount_for_user( [s.ledger, seller, token_id, quantity] ); - ledger = - MULTIASSET.Ledger.increase_token_amount_for_user( + ledger + = FA2Impl.Sidecar.increase_token_amount_for_user( [ledger, Tezos.get_source(), token_id, quantity] ); //update new offer @@ -181,44 +187,33 @@ const buy = ([token_id, quantity, seller]: [nat, nat, address], s: storage): ret { ...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 - ) + ledger: ledger } ] } - } - ) + } }; ``` -On `transfer,balance_of and update_ops` functions, change : - -- `owners: s.owners` by `owner_token_ids: s.owner_token_ids,token_ids: s.token_ids` -- `owners: ret2[1].owners` by `owner_token_ids: ret2[1].owner_token_ids,token_ids: ret2[1].token_ids` - Change the initial storage to ```ligolang #import "nft.jsligo" "Contract" -#import "@ligo/fa/lib/fa2/asset/multi_asset.jsligo" "MULTIASSET" -const default_storage = - { - administrators: Set.literal( - list(["tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb" as address]) - ) as set
, - offers: Map.empty as map<[address, nat], Contract.offer>, - ledger: Big_map.empty as MULTIASSET.Ledger.t, - metadata: Big_map.literal( - list( + +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`], [ - ["", bytes `tezos-storage:data`], - [ - "data", - bytes - `{ + "data", + bytes + `{ "name":"FA2 NFT Marketplace", "description":"Example of FA2 implementation", "version":"0.0.1", @@ -232,23 +227,19 @@ const default_storage = "errors": [], "views": [] }` - ] ] - ) - ) 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.owner, MULTIASSET.token_id]>, - 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.MultiAsset.operators, +}; ``` Compile again and deploy to ghostnet ```bash -TAQ_LIGO_IMAGE=ligolang/ligo:0.73.0 taq compile nft.jsligo +TAQ_LIGO_IMAGE=ligolang/ligo:1.0.0 taq compile nft.jsligo taq deploy nft.tz -e "testing" ``` @@ -256,11 +247,11 @@ taq deploy nft.tz -e "testing" ┌──────────┬──────────────────────────────────────┬───────┬──────────────────┬────────────────────────────────┐ │ Contract │ Address │ Alias │ Balance In Mutez │ Destination │ ├──────────┼──────────────────────────────────────┼───────┼──────────────────┼────────────────────────────────┤ -│ nft.tz │ KT1LwiszjMiEXasgtuHLswaMjUUdm5ARBmvk │ 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 @@ -275,7 +266,7 @@ 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 @@ -290,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 @@ -415,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 } @@ -768,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, @@ -815,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, @@ -857,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 ); @@ -988,7 +1004,7 @@ export default function OffersPage() { {"Description : " + nftContratTokenMetadataMap.get( - token_id.toNumber() + token_id.toString() )?.description} @@ -998,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); }} > @@ -1285,7 +1301,7 @@ export default function WineCataloguePage() { {"Description : " + nftContratTokenMetadataMap.get( - key[1].toNumber() + key[1].toString() )?.description} {"Seller : " + key[0]} @@ -1296,7 +1312,7 @@ export default function WineCataloguePage() { } title={ - nftContratTokenMetadataMap.get(key[1].toNumber())?.name + nftContratTokenMetadataMap.get(key[1].toString())?.name } /> Date: Fri, 13 Oct 2023 16:51:14 +0200 Subject: [PATCH 8/8] indentation fix ? --- src/pages/tutorials/build-an-nft-marketplace/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/tutorials/build-an-nft-marketplace/index.md b/src/pages/tutorials/build-an-nft-marketplace/index.md index 472f597c3..d01713ff1 100644 --- a/src/pages/tutorials/build-an-nft-marketplace/index.md +++ b/src/pages/tutorials/build-an-nft-marketplace/index.md @@ -21,7 +21,7 @@ You can find the 4 parts on github (solution + materials to build the UI) - [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