DEV Community

John Scott
John Scott

Posted on

Minting NFTs from IPFS using Thirdweb + Angular + MetaMask on Sepolia Testnet

Overview

This step-by-step tutorial will guide you through the entire process, from building an Angular app to securely storing your artwork on IPFS using Pinata to deploying and minting your very own NFT collection with Thirdweb's powerful NFT Collection smart contract platform.

All the tools and platforms used in this tutorial are free to use. As a developer, you should not incur any costs while following along.

At the end of the tutorial, the NFTs you generate will be visible in a collection on the OpenSea platform.

Before minting, make sure your MetaMask wallet is connected to Sepolia and has test ETH. If you do not have any ETH in your testnet wallet, go to https://sepoliafaucet.com to get free Sepolia ETH for this tutorial.


๐Ÿ”ง Tech Stack & Tools

  • Angular: Frontend framework
  • Pinata: Upload files to IPFS
  • Thirdweb: Web3 SDK + contract hosting
  • MetaMask: Browser wallet / authentication
  • OpenSea: NFT marketplace (Sepolia testnet)

โœ… Prerequisites

โš ๏ธ Installation of Node.js and Angular CLI is beyond the scope of this tutorial. It's assumed you already have the appropriate dev tools installed and set up prior to beginning.

  • Node.js v18+
  • Angular CLI: npm install -g @angular/cli
  • MetaMask (connected to Sepolia)
  • Test ETH: https://sepoliafaucet.com
  • Pinata account with JWT token (Web3 SDK section)
  • Thirdweb account

๐Ÿ”น Step 1: Upload Files to IPFS via Pinata

1.1 Setup Pinata account

  1. Create a Pinata API Key (JWT)

  2. Log into Pinata

  3. Go to "Developers" section โ†’ API Keys

  4. Click New Key

  • Name it nft-angular-app
  • Choose "admin" option to give it full permissions
  • After creating it, copy the JWT token โ€” you'll use it in your app.

1.2 Upload Images

  1. Navigate to the Files tab

  2. Manually upload your NFT image assets (JPG, PNG, etc.)

    • Leave privacy setting to Public

โš ๏ธ All image files uploaded under your account will appear in the Angular app, regardless of the project.


๐Ÿ”น Step 2: Create Thirdweb Project + Contract

  1. Go to https://thirdweb.com/dashboard
  2. Click Create Project and save your:
    • Client ID (frontend-safe)
    • Specify localhost:4200 in the list of accepted domains to limit access
    • Click Create
  3. Deploy a contract:
    • View your new project
    • Navigate to "Contracts"
    • Choose Prebuilt Contract > NFT Collection
    • Fill in name, symbol
    • Ensure contract is choose Sepolia
    • Deploy your contract -> you will be prompted to confirm the transaction using MetaMask
  4. Copy the deployed contract address โ€” youโ€™ll use this in Step 4 when we create our ThirdwebService class.

๐Ÿ”น Step 3: Angular App Setup

The following commands are cross-platform and should work the same on Windows, macOS, and Linux (provided Node.js and Angular CLI are properly installed). Run each command independantly as you will have some prompting between them.

โ— When prompted, say NO to SSR/Prerendering.
โš ๏ธ When adding material choose Yes to proceed and pick any pre-built theme and say Yes to setting up global typography.

ng new nft-mint-app --standalone --routing --style=scss
cd nft-mint-app
ng add @angular/material
npm install @thirdweb-dev/sdk ethers pinata-web3
Enter fullscreen mode Exit fullscreen mode

๐Ÿ”น Step 4: Thirdweb Service (src/app/thirdweb.service.ts)

Generate the Angular service:

ng generate service thirdweb --skip-tests
Enter fullscreen mode Exit fullscreen mode

Code for thirdweb.service.ts

โš ๏ธ Be sure to paste the contract address of your Thirdweb NFT contract in the placeholder provided

import { Injectable } from '@angular/core';
import { ThirdwebSDK } from '@thirdweb-dev/sdk';
import { ethers } from 'ethers';

@Injectable({ providedIn: 'root' })
export class ThirdwebService {
  private sdk: ThirdwebSDK | null = null;
  private contractAddress = '0xYourContractAddressHere';

  private async ensureSdk() {
    if (typeof window === 'undefined' || !(window as any).ethereum) {
      throw new Error('MetaMask is not available.');
    }
    await (window as any).ethereum.request({ method: 'eth_requestAccounts' });
    if (!this.sdk) {
      const provider = new ethers.providers.Web3Provider((window as any).ethereum);
      const signer = provider.getSigner();
      this.sdk = new ThirdwebSDK(signer, {
        clientId: '[THIRD-WEB-CLIENT-ID]', // Frontend-safe       
      });           
    }
  }

  async mintTo(walletAddress: string, metadata: { name: string; description: string; image: string }) {
    await this.ensureSdk();
    const contract = await this.sdk!.getContract(this.contractAddress, 'nft-drop');
    return contract.erc721.mintTo(walletAddress, metadata);
  }
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿ”น Step 5: Add NFT Minting Logic to AppComponent

Replace the contents of src/app/app.component.html

<mat-toolbar color="primary">NFT Minting App</mat-toolbar>
<div class="container">
  <h2 class="title">Mint Your NFT</h2>

  <p>
    This app allows you to mint your own NFT from an image that youโ€™ve uploaded to IPFS using Pinata.
    It uses <strong>Thirdweb</strong>โ€™s prebuilt ERC721 Drop contract and performs a lazy minting process.
    Thirdweb makes it simple to interact with Web3 and smart contracts using their SDK.
    Once minted, your NFT will be visible on OpenSea's Sepolia testnet.
  </p>

  <p>
    To get started, connect your MetaMask wallet using the button below.
    Your wallet must be connected to view available images and mint NFTs.
  </p>

  <div *ngIf="!walletConnected" style="margin-bottom: 2rem;">
    <button mat-flat-button color="accent" (click)="connectWallet()">Connect Wallet</button>
  </div>

  <ul *ngIf="uploadedFiles.length && walletConnected">
    <li *ngFor="let file of uploadedFiles" style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;">
      <a [href]="pinataConfig.pinataGateway + file.cid" target="_blank">
        <img [src]="pinataConfig.pinataGateway + '/ipfs/' + file.cid" alt="{{ file.name }}" style="height: 50px; border-radius: 4px;" />
      </a>
      <div style="flex-grow: 1">
        <a [href]="pinataConfig.pinataGateway + '/ipfs/' + file.cid" target="_blank">
          {{ file.name || file.cid }}
        </a>
      </div>
      <button mat-stroked-button color="primary" (click)="mintNft(file.cid, file.name)" [disabled]="mintedCids.has(file.cid) || minting">
        {{ mintedCids.has(file.cid) ? 'Minted' : 'Mint NFT' }}
      </button>
    </li>
  </ul>
  <p *ngIf="success">๐ŸŽ‰ NFT successfully minted!</p>
</div>
Enter fullscreen mode Exit fullscreen mode

Replace the contents of src/app/app.component.ts:

โš ๏ธ Make sure to paste in your Pinata gateway and JWT token from Step 1 in the placeholders provided in the code below

import { Component, OnInit } from '@angular/core';
import { ThirdwebService } from './thirdweb.service';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatButtonModule } from '@angular/material/button';
import { NgIf, NgFor } from '@angular/common';
import { PinataSDK } from 'pinata-web3';

interface Web3File {
  cid: string;
  name: string | null;
  metadataCid: string | null
}

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [MatToolbarModule, MatButtonModule, NgIf, NgFor],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss'
})
export class AppComponent implements OnInit {
  pinataConfig = {
    pinataGateway: 'https://[PINATA GATEWAY URL]',
    pinataJwt: '[PINATA JWT]'
  };
  pinataGateway = this.pinataConfig.pinataGateway + "/ipfs/";
  minting = false;
  success = false;
  uploadedFiles: Web3File[] = [];
  mintedCids = new Set<string>();
  walletConnected = false;

  pinata = new PinataSDK({ pinataJwt: this.pinataConfig.pinataJwt });

  constructor(private tw: ThirdwebService) {}

  async ngOnInit() {
    if (typeof window !== 'undefined' && (window as any).ethereum && (window as any).ethereum.selectedAddress) {
      this.walletConnected = true;
    }

    const list = await this.pinata.listFiles();

    this.uploadedFiles = list.map(file => ({
      cid: file.ipfs_pin_hash,
      name: file.metadata.name ?? '',
      metadataCid: null
    }));
  }

  async loadUploadedFiles() {
    const files = await this.pinata.listFiles() as any[]; // You can define a better type later
    this.uploadedFiles = files.map(meta => {

      return {
        name: meta.metadata.name,
        cid: meta.metadata.ipfs_pin_hash,
        metadataCid: meta.ipfs_pin_hash      
      } as Web3File;
    });
  }

  connectWallet() {
    if (typeof window !== 'undefined' && (window as any).ethereum) {
      (window as any).ethereum.request({ method: 'eth_requestAccounts' })
        .then(() => {
          this.walletConnected = true;
          console.log('Wallet connected');
        })
        .catch((error: any) => console.error('User denied wallet connection:', error));
    } else {
      alert('MetaMask is not installed.');
    }
  }

  async mintNft(cid: string, name: string |null) {
    this.success = false; //reset success flag
    this.minting = true;
    const metadata = {
      name: `RedRock NFT: ${name}`,
      description: 'Minted via Angular + Thirdweb',
      image: `ipfs://${cid}`
    };
    try {
      const accounts = await (window as any).ethereum.request({ method: 'eth_accounts' });
      const wallet = accounts[0];
      const tx = await this.tw.mintTo(wallet, metadata);
      console.log('Minted to wallet:', tx);
      this.mintedCids.add(cid);
      this.success = true;
    } catch (err) {
      console.error('Error minting:', err); 
    }
    this.minting = false;
  }
}
Enter fullscreen mode Exit fullscreen mode

Add a little style to your page:
Replace the contents of src/app/app.component.scss

.container {
    padding: 2rem;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 80vh;
    max-width: 600px;
    margin: 0 auto;
    line-height: 1.6;
    text-align: left;
  }

  .container .title {
    text-align: center;
  }

Enter fullscreen mode Exit fullscreen mode

๐Ÿ”น Step 6: Start Your Angular App

Once everything is configured, itโ€™s time to start your Angular app and try it out:

ng serve
Enter fullscreen mode Exit fullscreen mode

Then open your browser to: http://localhost:4200

You should now see the NFT Minting App interface.

๐Ÿ”น Step 6.5: Minting UI Behavior

When the app loads for the first time, youโ€™ll see an empty page with basic instructions.

Click 'Connect Wallet' button to authenticate via MetaMask. Once connected, the app will list your uploaded metadata files from Pinata.

Each listed item will:

  • Show the NFT name
  • Display a thumbnail preview
  • Include a 'Mint NFT' button beside it

Clicking Mint NFT button will:

  • Use the selected metadata file's CID
  • Pass the CID to the Thirdweb service, which interacts with your deployed contract
  • Trigger MetaMask to prompt you to confirm the minting transaction

The minted NFT will be stored on the Sepolia testnet and assigned to your connected wallet address.

Because the NFTs are authored using your Metamask wallet address, you are able to verify the NFT being created by looking in the Thirdweb dashboard and finding the NFTs in the project you created in Step 2.


๐Ÿ”น Step 7: View NFT on OpenSea

  1. Visit https://testnets.opensea.io
  2. Connect your MetaMask wallet by clicking the wallet icon in the top right
  3. Make sure you're on the Sepolia Testnet โ€” you may need to go to MetaMask settings and enable Show test networks to see Sepolia
  4. Search your wallet address or visit your profile to view the minted NFTs
  5. Your NFT may take a minute or two to appear...just be patient.

โœ… You Did It!

You've built an NFT minting dApp with:

  • ๐Ÿ“ฆ IPFS image + metadata hosting (Pinata)
  • โš™๏ธ Smart contract (Thirdweb)
  • ๐Ÿ” Wallet minting (MetaMask)
  • ๐Ÿ–ผ NFT viewing (OpenSea testnet)

๐Ÿ”— Resources

  • Thirdweb Dashboard
  • Pinata Web3 SDK
  • Thirdweb Docs
  • OpenSea Testnet
  • Sepolia Faucet

Top comments (0)