8 juillet 2022
Créer sa collection de NFT sur Ethereum
16 minutes de lecture
Dans l'article précédent, nous avons étudié les différentes caractéristiques et notions du Web 3.
Nous allons aujourd'hui créer notre première collection de NFT de A à Z sur la blockchain Ethereum accompagnée d'une dApp (Decentralized Application).
Créer sa collection de NFT accompagnée d'une dApp
Avant de commencer, il est important de préciser qu'il existe différents procédés pour créer sa collection de NFT :
- Des outils existent pour simplifier différentes tâches nécessaires au bon développement de notre collection.
Nous pouvons citer par exemple HashLips qui est un outil utilisé pour créer plusieurs œuvres d'art à partir de différentes couches (layers), qui seront ensuite superposées dans le calque de façon à générer des NFT uniques.
Il existe aussi des outils pour créer facilement notre propre dApp (application web qui intéragit avec un ou plusieurs smarts contracts).
Comme par exemple Moralis, qui fournit une dépendance et une API complète via un SDK très simple d'utilisation. L'API de Moralis nous permet d'obtenir :
- Les prix des NFT
- Données de propriété des NFT
- Métadonnées NFT
- Transférer un NFT
- et bien plus encore...
A savoir que toutes les fonctionnalités fournies par Moralis sont cross-chain par défaut 🤯
C'est à dire que les fonctionnalités sont compatibles avec différentes blockchains.
- Autrement, nous pouvons tout réaliser nous-même, et c'est ce que nous allons aborder dans la prochaine section de cet article.
Création du NFT
Pour cet exemple, nous allons nous focaliser sur la partie technique en utilisant le logo de Premier Octet comme image pour notre collection de NFT.
Avant de nous lancer dans le développement du smart-contract il va falloir créer et héberger nos métadonnées sur un IPFS.
IPFS qui veut dire InterPlanetary File System en anglais, est un système distribué de fichiers pair à pair qui ne dépend pas de serveurs centralisés. Son but est de connecter un ensemble d'équipements informatiques avec le même système de fichiers. D'une certaine manière, c'est ce qui va remplacer les bases de données couramment utilisées dans le Web 2.
Pour cela, il nous faudra créer un compte sur un NFT Storage pour héberger nos métadonnées, et utiliser la version gratuite de Pinata Cloud.
Une fois notre compte créé, nous pouvons nous rendre dans notre manager et cliquer sur le bouton Upload et sélectionner Folder :
Pour le dossier contenant toutes nos images ou métadonnées, il est important de respecter un index unique dans le nom de chaque fichier en démarrant de 0 et en incrémentant de + 1 pour chaque image.
Nous pouvons ensuite indiquer le chemin d'accès à nos images que nous avons créées précédemment :
Une fois les images sauvegardées dans Pinata, nous obtiendrons un numéro CID qui correspond au chemin d'accès à nos fichiers dans IPFS.
En se rendant sur cet URL, nous obtenons bien nos images.
Maintenant que nous avons nos images, nous pouvons créer les métadonnées des NFT qui sont en JSON, voici le modèle que nous allons utiliser pour notre exemple :
{
"name": "Premier Octet #0",
"description": "Premier Octet est une agence de développement d'applications mobiles basée à Paris.",
"attributes": [
{
"trait_type": "Techno",
"value": "React"
}
],
"image": "https://gateway.pinata.cloud/ipfs/QmYigrbExnVjTqvjy116zaSLMPhTxB8YXL3Jc8kMKRUELm/0.png"
}
Même manipulation que pour les images, nous pouvons désormais héberger notre dossier contenant nos métadonnées sur Pinata :
Maintenant que nos métadonnées sont bien hébergées sur un IPFS, nous pouvons commencer le développement du smart-contract !
Créer un smart contract
Avant de nous lancer dans le code, les smarts contracts sont par définition un programme automatisé ou littéralement un “contrat intelligent”. Ces contrats ou programmes sont des protocoles informatiques qui visent à automatiser une action lorsque les conditions prérequises sont remplies.
Dans notre cas, notre smart-contract va nous permettre de générer nos NFT directement sur la blockchain Ethereum. Nous allons utiliser le protocole ERC721A qui est une amélioration du protocole IERC721 pour permettre de générer plusieurs NFT en ayant un faible coût en frais de gas. C'est le protocole le plus répandu dans toutes les grandes collections NFT du moment.
Pour en savoir plus sur ce protocole, je vous invite à lire cet article.
Nous allons aussi utiliser OpenZeppelin qui fournit des produits sécurisés pour construire, automatiser et exploiter des applications décentralisées. Nous allons donc utiliser des smart-contract venant de OpenZeppelin pour développer notre contrat.
Dans cet exemple nous allons développer le smart-contract en Solidity car nous sommes sur la blockchain Ethereum. En Solana, nous aurions dû développer le smart-contract en Rust.
Pour tester, développer et déployer des smarts-contracts nous pouvons utiliser Remix IDE qui est un IDE Solidity en ligne ou bien, développer directement dans votre IDE favoris.
Pour ce projet, nous allons utiliser le smart-contract suivant :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;
/// @author Premier Octet https://www.premieroctet.com/
/// @title Premier Octet
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/finance/PaymentSplitter.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "./ERC721A.sol";
contract PremierOctetERC721A is Ownable, ERC721A, PaymentSplitter {
using Strings for uint;
// Toute les étapes de ventes
enum Step {
Before,
PublicSale,
SoldOut
}
// Phase de vente actuel
// 0 => Avant la vente, mint désactivé
// 1 => Vente active, mint activé
// 2 => Soldout, vente terminée
Step public sellingStep;
// Nombre total de NFT
uint private constant MAX_SUPPLY = 3;
// Prix de mint en Wei == 0.02 ETH
uint public publicSalePrice = 20000000000000000;
// l'url de votre IPFS correspondant à vos métadonnées (Pinata)
string public baseURI;
// Nombre de wallet dans la team
uint private teamLength;
// Lors du déploiement du contrat il faudra fournir les informations présentes dans le constructeur
constructor(
address[] memory _team,
uint[] memory _teamShares,
string memory _baseURI)
ERC721A("Premier Octet", "PO")
PaymentSplitter(_team, _teamShares) {
baseURI = _baseURI;
teamLength = _team.length;
}
// On vérifie que l'appel viens bien du bon contrat autrement on bloque la transaction
modifier callerIsUser() {
require(tx.origin == msg.sender, "The caller is another contract");
_;
}
// Mint un NFT
function publicSaleMint(address _account, uint _quantity) external payable callerIsUser {
uint price = publicSalePrice;
require(price != 0, "Price is 0");
require(sellingStep == Step.PublicSale, "Public sale is not activated");
require(totalSupply() + _quantity <= MAX_SUPPLY, "Max supply exceeded");
require(msg.value >= price * _quantity, "Not enought funds");
_safeMint(_account, _quantity);
}
// Modifier le prix de Mint
function setPublicSalePrice(uint _publicSalePrice) external onlyOwner {
publicSalePrice = _publicSalePrice;
}
// Obtenir l'URI du jeton d'un NFT par son ID
function tokenURI(uint _tokenId) public view virtual override returns (string memory) {
require(_exists(_tokenId), "URI query for nonexistent token");
return string(abi.encodePacked(baseURI, _tokenId.toString(), ".json"));
}
// Changer la phase de vente
function setStep(uint _step) external onlyOwner {
sellingStep = Step(_step);
}
// Envoyer tout les Ethereum présents dans le contrat sur les adresses des membres de la team
function releaseAll() external onlyOwner {
for(uint i = 0 ; i < teamLength ; i++) {
release(payable(payee(i)));
}
}
}
Pour le déployer nous allons initialiser notre projet avec Hardhat en exécutant ces commandes :
mkdir my-nft-project
cd my-nft-project
npm init --yes
npm install --save-dev hardhat
Dans le même dossier où nous avons installé Hardhat, exécutons la commande suivante :
npx hardhat
Puis sélectionnons Create an empty hardhat.config.js
:
Un fichier hardhat.config.js
est généré, nous pouvons le modifier dès maintenant en assignant ces valeurs :
require('@nomiclabs/hardhat-waffle')
require('@nomiclabs/hardhat-etherscan')
require('hardhat-gas-reporter')
require('dotenv').config()
const { API_URL, PRIVATE_KEY, ETHERSCAN_API_KEY } = process.env
/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
solidity: {
version: '0.8.12',
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
paths: {
artifacts: './artifacts',
},
defaultNetwork: 'rinkeby',
networks: {
hardhat: {
chainId: 137,
},
rinkeby: {
url: API_URL,
accounts: [`0x${PRIVATE_KEY}`],
},
},
gasReporter: {
currency: 'EUR',
gasPrice: 21,
enabled: true,
},
etherscan: {
apiKey: ETHERSCAN_API_KEY,
},
}
Il est nécessaire d'installer les dépendances suivantes :
yarn add -D @nomiclabs/hardhat-waffle @nomiclabs/hardhat-etherscan hardhat-gas-reporter dotenv
Il faudra aussi ajouter les variables d'environnement :
API_URL
: Un compte sur Infura doit être créé pour obtenir une clé API.PRIVATE_KEY
: Correspond à la clé secrète du wallet, propriétaire du contrat qui sera déployé. Cette clé doit absolument rester secrète !ETHERSCAN_API_KEY
: Un compte sur Etherscan doit aussi être créé pour obtenir une clé API.
Nous y sommes presque ! Il reste à ajouter le script de déploiement, nous pouvons créer un dossier script
à la racine du projet et ajouter à l'intérieur de ce dossier un fichier deploy.js
:
const hre = require('hardhat')
async function main() {
// Wallet des membres de la team
let team = ['0x2939cd2d52D6aB2B3cBD3966dA800E7dea69e955']
// Pourcentage d'ETH que l'addresse va récupérer après la vente
let teamShares = [100]
// Lien des métadonnées IPFS (Pinata)
let baseURI = 'https://gateway.pinata.cloud/ipfs/QmX7KKoebxvcpjLqhQ2FhX7bQkTe8uea7iaisYWfy1gWxW/'
// Déploiement du contrat
const Raffle = await hre.ethers.getContractFactory('PremierOctetERC721A')
const raffle = await Raffle.deploy(team, teamShares, baseURI)
await raffle.deployed()
console.log('Premier Octet Contract ERC721A deployed to :' + raffle.address)
console.log(team, teamShares, baseURI)
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error)
process.exit(1)
})
Ajoutons maintenant le smart-contract à la racine du projet dans un dossier contracts
de façon à avoir une structure de fichier comme l'exemple ci-dessous :
Sans oublier les dépendances nécessaires au déploiement du contrat :
yarn add -D @nomiclabs/hardhat-ethers @nomiclabs/hardhat-etherscan @nomiclabs/hardhat-waffle @openzeppelin/contracts
On peut ajouter les scripts dans le fichier package.json
:
"scripts": {
"deploy-contract": "npx hardhat run script/deploy.js --network rinkeby",
"verify": "npx hardhat verify --network rinkeby --constructor-args arguments.js contractAddress"
},
Pour pouvoir exécuter la commande suivante :
yarn deploy-contract
Et voilà le smart-contract est déployé sur le réseau de test Rinkeby ! Nous pouvons copier coller l'adresse et la retrouver dans Etherscan Rinkeby.
Pour vérifier le contrat sur Etherscan, remplaçons contractAddress
dans le script verify
du fichier package.json
par l'adresse du contrat qui vient d'être publiée :
"verify": "npx hardhat verify --network rinkeby --constructor-args arguments.js 0x6B5067c2F4FbFa3711d4EF76237219CBe554A1B2"
Créons un fichier arguments.js
à la racine du projet contenant les paramètres passés dans le constructeur lors du déploiement du contrat :
module.exports = [
['0x2939cd2d52D6aB2B3cBD3966dA800E7dea69e955'],
[100],
'https://gateway.pinata.cloud/ipfs/QmX7KKoebxvcpjLqhQ2FhX7bQkTe8uea7iaisYWfy1gWxW/',
]
Exécutons ensuite cette commande afin de vérifier le smart-contract pour pouvoir accéder au code source depuis Etherscan :
yarn verify
Une fois le smart-contract validé, une petite bulle verte apparaîtra dans Etherscan :
Le smart contract est désormais correctement déployé et vérifié, nous pouvons directement passer à la prochaine étape qui consistera à créer une app en React qui va communiquer avec notre smart-contract.
Création d'une app NextJS
Pour initialiser notre app NextJS nous pouvons exécuter la commande suivante à la racine de votre projet :
yarn create next-app --typescript
Organisons les dossiers et fichiers de façon à pouvoir les retrouver facilement à la racine du projet comme le montre l'exemple ci-dessous :
Installons ensuite ChakraUI dans le projet afin de créer des jolis designs ✨
yarn add @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^6
Une fois ChakraUI d'installé, il faudra mettre en place le provider dans le fichier _app.tsx
comme ceci :
import type { AppProps } from 'next/app'
import { ChakraProvider } from '@chakra-ui/react'
import '../styles/globals.css'
function MyApp({ Component, pageProps }: AppProps) {
return (
<ChakraProvider>
<Component {...pageProps} />
</ChakraProvider>
)
}
export default MyApp
Nous pouvons maintenant créer notre UI et une Navbar dans un dossier components
:
import { Button, Flex, Image, Spacer, Text } from '@chakra-ui/react'
const Navbar = () => {
return (
<Flex bgColor="black" py={5} px={[5, 5, 20, 20]} w="100%" align="center" justify="center">
<Image src="/logo.jpg" borderRadius={5} alt="PremierOctet logo" width={[45, 45, 75, 75]} />
<Text ml={5} color="white" fontSize={[15, 15, 25, 25]} fontWeight={800}>
Premier Octet NFT
</Text>
<Spacer />
<Button colorScheme="blue" fontWeight={800} size="md">
Connect Wallet
</Button>
</Flex>
)
}
export default Navbar
Un affichage semblable à cette image apparaît :
Connecter son wallet sur son app en React
Nous allons maintenant ajouter la possibilité de connecter son wallet en cliquant sur le bouton Connect Wallet créé précédemment.
Il existe différentes façon de connecter son wallet. L'option basique est d'utiliser la librairie ethers qui fonctionnera seulement avec Metamask qui est un provider directement installé dans le navigateur web.
L'option plus complète consiste à utiliser ethers ainsi que WalletConnect accompagné de la Web3Modal. Cette option offre la possibilité de connecter tout type de Wallet et pas seulement Metamask, citons par exemple Coinbase Wallet ou Torus.
Pour cela, il faudra installer les dépendances suivantes :
yarn add ethers @walletconnect/web3-provider web3modal
Pour pouvoir facilement utiliser le Web3 dans notre projet, créons un dossier context à la racine du projet qui contiendra ethersProviderContext.tsx
:
import React, { useCallback, useEffect, useState } from 'react'
import Web3Modal from 'web3modal'
import WalletConnectProvider from '@walletconnect/web3-provider'
import { useRouter } from 'next/router'
import { providers } from 'ethers'
interface AppContext {
address: string
provider: providers.Web3Provider
disconnect: any
connect: any
chainId: number
loading: boolean
}
const EthersContext = React.createContext<AppContext | null>(null)
export const EthersProvider = ({ children }: any) => {
const [address, setAddress] = useState<null | string>(null)
const [provider, setProvider] = useState<any>(null)
const [loading, setLoading] = useState<boolean>(false)
const [web3Provider, setWeb3Provider] = useState<null | providers.Web3Provider>(null)
const [chainId, setChainId] = useState<null | number>(null)
const router = useRouter()
let web3Modal: Web3Modal
if (typeof window !== 'undefined') {
const providerOptions = {
walletconnect: {
display: {
name: 'Mobile',
},
package: WalletConnectProvider,
options: {
infuraId: process.env.NEXT_PUBLIC_INFURA_API_KEY,
},
},
}
web3Modal = new Web3Modal({
network: 'rinkeby',
cacheProvider: true,
providerOptions,
disableInjectedProvider: false,
})
}
const connect = useCallback(async function () {
try {
setLoading(true)
const provider = await web3Modal.connect()
const web3Result = new providers.Web3Provider(provider)
const signer = web3Result.getSigner()
const address = await signer.getAddress()
const network = await web3Result.getNetwork()
setProvider(provider)
setWeb3Provider(web3Result)
setAddress(address)
setChainId(network.chainId)
setLoading(false)
} catch {
console.log('error')
}
}, [])
const disconnect = useCallback(
async function () {
setLoading(true)
await web3Modal.clearCachedProvider()
if (provider) {
if (provider.disconnect && typeof provider.disconnect === 'function') {
await provider.disconnect()
}
}
setAddress(null)
setProvider(null)
setWeb3Provider(null)
setChainId(null)
setLoading(false)
},
[provider]
)
useEffect(() => {
if (provider?.on) {
const handleAccountsChanged = (accounts: any) => {
console.log('accountsChanged', accounts)
setAddress(accounts[0])
}
const handleChainChanged = (_hexChainId: any) => {
router.reload()
}
const handleDisconnect = (error: any) => {
console.log('disconnect', error)
disconnect()
}
provider.on('accountsChanged', handleAccountsChanged)
provider.on('chainChanged', handleChainChanged)
provider.on('disconnect', handleDisconnect)
return () => {
if (provider.removeListener) {
provider.removeListener('accountsChanged', handleAccountsChanged)
provider.removeListener('chainChanged', handleChainChanged)
provider.removeListener('disconnect', handleDisconnect)
}
}
}
}, [provider, disconnect])
return (
<EthersContext.Provider
value={{
address: address!,
provider: web3Provider!,
disconnect: disconnect,
connect: connect,
chainId: chainId!,
loading: loading,
}}
>
{children}
</EthersContext.Provider>
)
}
export default EthersContext
Pensez à bien ajouter votre clé API Infura dans les variables d'environnement (NEXT_PUBLIC_INFURA_API_KEY)
Grâce à ce contexte, nous pourrons nous connecter, nous déconnecter ou bien détecter un changement de réseau.
Notre fichier useEthersProvider.ts
doit être sauvegardé dans un dossier hooks
:
import { useContext } from 'react'
import EthersContext from '../context/ethersProviderContext'
export default function useEthersProvider() {
const context = useContext(EthersContext)
if (!context) {
throw new Error('useEthersProvider must be used within a EthersProvider')
}
return context
}
Nous pouvons maintenant ajouter EthersProvider
dans notre fichier _app.tsx
:
import type { AppProps } from 'next/app'
import { ChakraProvider } from '@chakra-ui/react'
import { EthersProvider } from '../context/ethersProviderContext'
import '../styles/globals.css'
function MyApp({ Component, pageProps }: AppProps) {
return (
<EthersProvider>
<ChakraProvider>
<Component {...pageProps} />
</ChakraProvider>
</EthersProvider>
)
}
export default MyApp
Voilà ! L'implémentation Web 3 est bien ajoutée au projet, on peut dés à présent importer les functions connect
et disconnect
dans le composant Navbar
créé précédemment :
import { Button, Flex, Image, Spacer, Text } from '@chakra-ui/react'
import useEthersProvider from '../../hooks/useEthersProvider'
const Navbar = () => {
const { connect, address, disconnect } = useEthersProvider()
return (
<Flex bgColor="black" py={5} px={[5, 5, 20, 20]} w="100%" align="center" justify="center">
<Image src="/logo.jpg" borderRadius={5} alt="PremierOctet logo" width={[45, 45, 75, 75]} />
<Text ml={5} color="white" fontSize={[15, 15, 25, 25]} fontWeight={800}>
Premier Octet NFT
</Text>
<Spacer />
{address ? (
<Button onClick={() => disconnect()} fontWeight={800} colorScheme="blue">
{address.substring(0, 6)}...
{address.substring(address.length - 4, address.length)}
</Button>
) : (
<Button onClick={() => connect()} fontWeight={800} colorScheme="blue">
Connect Wallet
</Button>
)}
</Flex>
)
}
export default Navbar
Le wallet se connecte bien à notre site web ! 🥳
Intéragir avec le smart-contract
Une fois notre wallet connecté, il va falloir le faire communiquer avec notre smart-contract pour générer un NFT.
Lorsque nous avons déployé le contrat dans la section précédente, un dossier artifacts
a été généré à la racine du projet contenant toutes les informations relatives à notre contrat, c'est cette partie qui va nous être utile.
Nous allons récupérer le contract ABI (Application Binary Interface) qui correspond à toutes les fonctions / inputs présents dans le contrat pour que nous puissons communiquer avec.
Importons le contract ABI dans le fichier index.tsx
de cette façon :
import contractABI from '../artifacts/contracts/PremierOctetERC721A.sol/PremierOctetERC721A.json'
Nous pouvons instancier notre contrat dans notre code comme ceci :
const { provider } = useEthersProvider()
const mint = async () => {
const signer = provider.getSigner()
const contract = new ethers.Contract(
'0x6B5067c2F4FbFa3711d4EF76237219CBe554A1B2',
contractABI.abi,
signer
)
}
La signature numérique du wallet est obligatoire pour autoriser un wallet à exécuter une transaction.
Génération d'un NFT
Une fois notre contrat instancié nous pouvons commencer à communiquer avec, et compléter la function suivante :
const [isLoading, setIsLoading] = useState<boolean>(false);
const mint = async () => {
setIsLoading(true)
const signer = provider.getSigner();
const contract = new ethers.Contract(
"0x6B5067c2F4FbFa3711d4EF76237219CBe554A1B2",
contractABI.abi,
signer
);
const overrides = {
from: address,
// Doit correspondre au prix à payer pour exécuter la transaction en Wei
// => soit 0.02 ETH
value: "20000000000000000",
};
try {
const mintNft = await contract.publicSaleMint(address, 1, overrides);
await mintNft.wait();
setIsLoading(false)
} catch (err) {
console.log(err);
setIsLoading(false)
}
};
Ici nous appelons la function publicSaleMint
présente dans notre smart contract, en passant en paramètre l'adresse de l'utilisateur ainsi que la quantité de NFT à générer.
Nous pouvons désormais ajouter un bouton sur la page d'accueil pour générer un NFT si le wallet est bien connecté :
<Flex w="100%" align="center" my={20} justify="center">
{isLoading ? (
<Spinner colorScheme="blue" />
) : address ? (
<Button onClick={() => mint()} fontWeight={800} colorScheme="blue">
Mint an NFT
</Button>
) : (
<Text fontSize={30} fontWeight="bold">
Vous devez connecter votre wallet
</Text>
)}
</Flex>
Voilà nous avons déjà une très bonne base ! Nous pouvons maintenant ajouter une fonction pour changer l'étape de vente du smart-contract pour autoriser le mint d'un NFT, car actuellement la vente n'est pas autorisée.
Nous pouvons ajouter cette fonction :
const activateSale = async () => {
setIsLoading(true)
const signer = provider.getSigner()
const contract = new ethers.Contract(
'0x6B5067c2F4FbFa3711d4EF76237219CBe554A1B2',
contractABI.abi,
signer
)
try {
const activeSale = await contract.setStep(1)
await activeSale.wait()
setIsLoading(false)
} catch (err) {
console.log(err)
setIsLoading(false)
}
}
Accompagné d'un bouton pour l'appeler :
<Button onClick={() => activateSale()} fontWeight={800} colorScheme="blue" mx={10}>
Start Sale
</Button>
Pour ce test, nous sommes sur le réseau de test Rinkeby, des ETH de test Rinkeby sont disponibles gratuitement ici.
Voici une démo des démarches à suivre :
Une fois la vente activée nous pouvons apercevoir une nouvelle entrée sur Etherscan qui indique que nous avons bien appelé la méthode setStep
:
Nous pouvons dès maintenant générer notre premier NFT !
Un clique sur le bouton Mint an NFT
comme le montre l'exemple ci-dessous :
Pour cet exemple, nous allons répéter l'opération deux fois pour générer les deux NFT restants, car nous avons assigné un maximum de 3 NFT dans le smart-contract.
Une fois tous les NFT générés, ils apparaisssent dans le Opensea réservé au Testnet Rinkeby juste ici.
On y retrouve aussi les métadonnées que nous avons créées auparavant dans les propriétés du NFT :
Mission réussie ! Nous avons créé notre première collection NFT de A à Z 😍
L'avenir des NFT
Depuis plusieurs mois, et malgré quelques initiatives comme le récent lancement d’une collection officielle imaginée par le Vatican, le marché des NFT connaît un sérieux coup de mou. Entre février et mars 2022, les transactions enregistrées sur la blockchain sont passées de 3,9 milliards à seulement 964 millions de dollars.
Cependant, de plus en plus de plateformes permettent aux utilisateurs de créer, d’acquérir, de vendre ou de collectionner des NFT. Parallèlement, le nombre de nouveaux acquéreurs de NFT augmente également. Tout semble donc possible.
Tout le code source du projet est disponible sur Github ainsi que la collection générée sur Opensea.
Vous avez maintenant toutes les ressources nécessaires pour créer votre propre collection de NFT !
À vous de jouer ! 🤠
👋🏼