> For the complete documentation index, see [llms.txt](https://docs.chiliz.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.chiliz.com/fr/developper/notions-avancees/travailler-avec-les-nfts/minter-avec-viem.md).

# Minter avec viem

Mintez des NFTs sur Chiliz Chain de manière programmatique en utilisant TypeScript avec viem ou ethers.js, un contrat ERC-721 déployé sur Remix, et Pinata pour l'hébergement IPFS.

Pour les développeurs qui préfèrent un contrôle granulaire sur le processus de minting, vous pouvez utiliser TypeScript vanilla avec [viem](https://viem.sh/), une interface TypeScript pour les projets EVM. La [bibliothèque Ethers.js](https://ethers.org/) est également une approche solide.

Adopter l'approche *vanilla* nécessite un peu plus de travail que lorsqu'on s'appuie sur des frameworks comme thirdweb. Par exemple, à moins d'exploiter votre propre nœud IPFS, vous devrez vous appuyer sur des plateformes de stockage NFT, qui sont payantes.

{% hint style="success" %}
Cet exemple utilise [Pinata](https://pinata.cloud/) comme hébergeur IPFS.\
Vous devrez également déployer vous-même un [contrat OpenZeppelin ERC-721](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol) sur Chiliz Chain. [Remix IDE](https://remix.ethereum.org/) vous offre un IDE dans le navigateur pour le faire. N'oubliez pas de vérifier le contrat en utilisant un explorateur de blocs !
{% endhint %}

Maintenant, installons `viem`, le `Pinata SDK` et le module `dotenv` :

```bash
npm i viem pinata dotenv
```

Configurez votre fichier `.env` pour qu'il corresponde à vos besoins :

```
# Portefeuille / contrat
PRIVATE_KEY=0xabc...                      # côté serveur uniquement
CONTRACT_ADDRESS=0xYourErc721Address
RECIPIENT=0xRecipientOrLeaveEmpty         # optionnel ; par défaut au minteur

# Pinata
PINATA_JWT=eyJhbGciOi...                  # JWT depuis le tableau de bord Pinata
PINATA_GATEWAY=your-subdomain.mypinata.cloud

# Mint unique
IMAGE_PATH=./art/image.png
NAME=My Chiliz NFT
DESCRIPTION=Minted on Chiliz with viem

# Mint en lot
IMAGES_DIR=./art
NAME_PREFIX=My Chiliz NFT
BATCH_DESCRIPTION=Batch minted on Chiliz with viem
```

Maintenant, plongeons dans le code.\
*Inspirez-vous-en, ne l'utilisez pas tel quel !*

## Minter un seul NFT

{% code overflow="wrap" lineNumbers="true" fullWidth="true" %}

```typescript
import 'dotenv/config';
import fs from 'fs';
import path from 'path';

import { createWalletClient, createPublicClient, http, parseEventLogs } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import type { Address } from 'viem';
import { chiliz, spicy } from 'viem/chains';
import { PinataSDK } from 'pinata';

// ABI minimal : safeMint(to, uri) + tokenURI + Transfer
const abi = [
  { type: 'function', name: 'safeMint', stateMutability: 'nonpayable',
    inputs: [{ name: 'to', type: 'address' }, { name: 'uri', type: 'string' }], outputs: [] },
  { type: 'function', name: 'tokenURI', stateMutability: 'view',
    inputs: [{ name: 'tokenId', type: 'uint256' }], outputs: [{ type: 'string' }] },
  { type: 'event', name: 'Transfer',
    inputs: [
      { name: 'from', type: 'address', indexed: true },
      { name: 'to', type: 'address', indexed: true },
      { name: 'tokenId', type: 'uint256', indexed: true },
    ]},
] as const;

function guessMime(p: string) {
  const ext = path.extname(p).toLowerCase();
  if (ext === '.png') return 'image/png';
  if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg';
  if (ext === '.webp') return 'image/webp';
  if (ext === '.gif') return 'image/gif';
  if (ext === '.mp4') return 'video/mp4';
  if (ext === '.webm') return 'video/webm';
  return 'application/octet-stream';
}

async function main() {
  // Choisissez votre réseau : utilisez `spicy` pour Spicy Testnet, changez pour `chiliz` pour le Mainnet.
  const chain = spicy; // Changez pour `chiliz` pour le Mainnet

  const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
  const walletClient = createWalletClient({ account, chain, transport: http() });
  const publicClient = createPublicClient({ chain, transport: http() });

  const address = process.env.CONTRACT_ADDRESS as Address;
  const recipient = (process.env.RECIPIENT as Address) || account.address;

  // Téléversement IPFS (Pinata)
  const pinata = new PinataSDK({
    pinataJwt: process.env.PINATA_JWT!,
    pinataGateway: process.env.PINATA_GATEWAY!,
  });

  const filePath = process.env.IMAGE_PATH!;
  const fileBlob = new Blob([fs.readFileSync(filePath)], { type: guessMime(filePath) });
  const fileObj = new File([fileBlob], path.basename(filePath), { type: guessMime(filePath) });

  const up = await pinata.upload.public.file(fileObj);
  const imageUri = `ipfs://${up.cid}`;

  const meta = await pinata.upload.public.json({
    name: process.env.NAME!,
    description: process.env.DESCRIPTION!,
    image: imageUri,
  });
  const metadataUri = `ipfs://${meta.cid}`;

  // Mint on-chain via viem
  const txHash = await walletClient.writeContract({
    address,
    abi,
    functionName: 'safeMint',
    args: [recipient, metadataUri],
  });

  const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });

  const ZERO = '0x0000000000000000000000000000000000000000';
  const logs = parseEventLogs({ abi, logs: receipt.logs, eventName: 'Transfer' });
  const mintLog = logs.find(l => (l.args as any).from?.toLowerCase?.() === ZERO);
  const tokenId = mintLog ? (mintLog.args as any).tokenId : undefined;

  console.log('tx:', txHash, '| tokenId:', tokenId ?? '(not parsed)', '| tokenURI:', metadataUri);
}

main().catch(err => (console.error(err), process.exit(1)));

```

{% endcode %}

## Minter une collection de NFTs

{% code overflow="wrap" lineNumbers="true" fullWidth="true" %}

```typescript
import 'dotenv/config';
import fs from 'fs';
import path from 'path';
// import { Blob } from 'buffer'; // Node 18 : décommentez la ligne suivante pour obtenir Blob.

import { createWalletClient, createPublicClient, http, parseEventLogs } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import type { Address } from 'viem';
import { chiliz, spicy } from 'viem/chains';
import { PinataSDK } from 'pinata';

// ABI minimal : safeMint(to, uri) + tokenURI + Transfer
const abi = [
  { type: 'function', name: 'safeMint', stateMutability: 'nonpayable',
    inputs: [{ name: 'to', type: 'address' }, { name: 'uri', type: 'string' }], outputs: [] },
  { type: 'function', name: 'tokenURI', stateMutability: 'view',
    inputs: [{ name: 'tokenId', type: 'uint256' }], outputs: [{ type: 'string' }] },
  { type: 'event', name: 'Transfer',
    inputs: [
      { name: 'from', type: 'address', indexed: true },
      { name: 'to', type: 'address', indexed: true },
      { name: 'tokenId', type: 'uint256', indexed: true },
    ]},
] as const;

function guessMime(p: string) {
  const ext = path.extname(p).toLowerCase();
  if (ext === '.png') return 'image/png';
  if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg';
  if (ext === '.webp') return 'image/webp';
  if (ext === '.gif') return 'image/gif';
  if (ext === '.mp4') return 'video/mp4';
  if (ext === '.webm') return 'video/webm';
  return 'application/octet-stream';
}

async function main() {
  // Choisissez votre réseau : utilisez `spicy` pour Spicy Testnet, changez pour `chiliz` pour le Mainnet.
  const chain = spicy; // Changez pour `chiliz` pour le Mainnet

  const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
  const walletClient = createWalletClient({ account, chain, transport: http() });
  const publicClient = createPublicClient({ chain, transport: http() });

  const address = process.env.CONTRACT_ADDRESS as Address;
  const recipient = (process.env.RECIPIENT as Address) || account.address;

  // Téléversement IPFS (Pinata)
  const pinata = new PinataSDK({
    pinataJwt: process.env.PINATA_JWT!,
    pinataGateway: process.env.PINATA_GATEWAY!, // optionnel ; pas nécessaire pour les URIs ipfs://
  });

  const filePath = process.env.IMAGE_PATH!;
  const fileBlob = new Blob([fs.readFileSync(filePath)], { type: guessMime(filePath) });

  // Passer un Blob directement
  const up = await pinata.upload.public.file(fileBlob);
  const imageUri = `ipfs://${up.cid}`;

  const meta = await pinata.upload.public.json({
    name: process.env.NAME!,
    description: process.env.DESCRIPTION!,
    image: imageUri,
  });
  const metadataUri = `ipfs://${meta.cid}`;

  // Mint on-chain via viem
  const txHash = await walletClient.writeContract({
    address,
    abi,
    functionName: 'safeMint',
    args: [recipient, metadataUri],
  });

  const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });

  const ZERO = '0x0000000000000000000000000000000000000000';
  const logs = parseEventLogs({ abi, logs: receipt.logs, eventName: 'Transfer' });
  const mintLog = logs.find(l => (l.args as any).from?.toLowerCase?.() === ZERO);
  const tokenId = mintLog ? (mintLog.args as any).tokenId : undefined;

  console.log('tx:', txHash, '| tokenId:', tokenId ?? '(not parsed)', '| tokenURI:', metadataUri);
}

main().catch(err => (console.error(err), process.exit(1)));

```

{% endcode %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.chiliz.com/fr/developper/notions-avancees/travailler-avec-les-nfts/minter-avec-viem.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
