NFTWallet API

This solutions guide explains how to deploy ERC-721 and ERC-1155 NFTs on the Base Sepolia network using the Coinbase Developer Platform (CDP) SDK and Pinata’s IPFS storage. The app includes a Next.js frontend and Flask backend to manage token metadata, collection deployment, and automatic minting.

This guide demonstrates how to use the CDP SDK to deploy NFT (ERC-721) and MultiToken (ERC-1155) contracts on Base Sepolia. The CDP SDK provides a powerful way to handle wallet creation, contract deployment, and token minting.

Complete Implementation For a full implementation including:

  • IPFS metadata management with Pinata
  • React frontend interface
  • Token collection management

Check out:

This sample app is for demonstration purposes only. Make sure to persist your private keys, and to use testnet. Secure your wallet using best practices. In production, you should use the 2-of-2 CDP Server-Signer with IP whitelisting for your Secret API key for increased security.

Features

  • Deploy both ERC-721 and ERC-1155 NFT contracts
  • Support for collections with multiple tokens
  • Automatic token minting after deployment
  • Real-time deployment progress tracking
  • Upload and manage token metadata through IPFS via Pinata
  • Automatic wallet creation and funding with testnet ETH

Prerequisites

Before you begin, make sure you have the following:

  • Installed the CDP SDK
  • Provision a CDP API Key and Secret API Key.
  • Create a Pinata account and get API key, JWT, and Gateway URL for IPFS storage
  • Node.js, npm, and Python installed on your system

Step-by-Step Guide

Step 1: Set Up Environment Variables

Define environment variables for both frontend and backend configurations.

Frontend .env.local:

NEXT_PUBLIC_PINATA_JWT=Your-Pinata-JWT
NEXT_PUBLIC_PINATA_GATEWAY=Your-Pinata-Gateway-URL
NEXT_PUBLIC_PINATA_API_KEY=Your-Pinata-Gateway-APIkey
NEXT_PUBLIC_PINATA_SECRET_API_KEY=Your-Pinata-Gateway-SECRET-API-KEY

Backend .env:

CDP_API_KEY_NAME=Your-CDP-API-Key-Name
CDP_API_PRIVATE_KEY=Your-CDP-Private-Key

Step 2: CDP SDK Implementation

Here’s how to use the CDP SDK to deploy NFT contracts:

from flask import Flask, request, jsonify
from flask_cors import CORS
import os
from dotenv import load_dotenv
import requests
from cdp import Cdp, Wallet
import time
import traceback

# Initialize Flask app with CORS support for cross-origin requests
app = Flask(__name__)
CORS(app, resources={r"/api/*": {"origins": "*"}})

# Load environment variables from .env file
load_dotenv()

# Get required API keys from environment variables
CDP_API_KEY_NAME = os.getenv('CDP_API_KEY_NAME')
CDP_API_PRIVATE_KEY = os.getenv('CDP_API_PRIVATE_KEY')
PINATA_API_KEY = os.getenv('PINATA_API_KEY')
PINATA_SECRET_API_KEY = os.getenv('PINATA_SECRET_API_KEY')

# Configure CDP SDK with API credentials
if not CDP_API_KEY_NAME or not CDP_API_PRIVATE_KEY:
    print("Missing CDP API credentials in environment variables")
    raise EnvironmentError("CDP API credentials are required.")

Cdp.configure(CDP_API_KEY_NAME, CDP_API_PRIVATE_KEY)

# Track deployment progress for frontend updates
deployment_status = {
    "step": "",
    "status": ""
}

def update_deployment_status(step, status):
    """Helper function to update and log deployment status"""
    deployment_status["step"] = step
    deployment_status["status"] = status
    print(f"Status Update: {step} - {status}")

def create_funded_wallet():
    """
    Creates a new wallet on Base Sepolia testnet and funds it with test ETH.
    Includes retry logic for faucet requests.
    
    Returns:
        Wallet: A CDP SDK wallet object with sufficient test ETH
    """
    max_retries = 3
    retry_delay = 5

    try:
        # Create new wallet
        update_deployment_status("Creating new wallet on Base Sepolia...", "loading")
        wallet = Wallet.create(network_id="base-sepolia")
        print(f"Created wallet: {wallet.default_address.address_id}")

        # Check if wallet already has sufficient funds
        initial_balance = float(wallet.balance("eth"))
        if initial_balance >= 0.01:
            update_deployment_status("Wallet funded successfully.", "complete")
            return wallet

        # Request testnet ETH from faucet with retry logic
        update_deployment_status("Requesting testnet ETH from faucet...", "loading")
        
        for attempt in range(max_retries):
            try:
                wallet.faucet()
                time.sleep(10)  # Wait for faucet transaction to complete
                
                current_balance = float(wallet.balance("eth"))
                if current_balance >= 0.01:
                    update_deployment_status("Wallet funded successfully.", "complete")
                    return wallet
                
            except Exception as e:
                print(f"Faucet attempt {attempt + 1} failed: {str(e)}")
                if attempt < max_retries - 1:
                    time.sleep(retry_delay)
                else:
                    raise Exception("Failed to fund wallet after multiple attempts")

        raise Exception("Failed to receive sufficient testnet ETH")

    except Exception as e:
        error_msg = str(e)
        print(f"Error in create_funded_wallet: {error_msg}")
        update_deployment_status("Creating new wallet on Base Sepolia...", "error")
        raise Exception(error_msg)

@app.route('/api/deploy', methods=['POST'])
def deploy_contract():
    """
    Main endpoint for deploying NFT contracts.
    Handles both ERC721 (single NFTs) and ERC1155 (multi-token) deployments.
    
    Expected POST data:
    {
        "type": "ERC721" or "ERC1155",
        "name": "Contract Name",  # For ERC721
        "symbol": "SYMBOL",       # For ERC721
        "baseUri": "ipfs://<hash>/",
        "tokenCount": number      # For ERC1155
    }
    """
    try:
        data = request.get_json()
        print("\nReceived deployment request:", data)

        # Create and fund a new wallet for deployment
        wallet = create_funded_wallet()
        if not wallet:
            return jsonify({"success": False, "error": "Failed to create wallet with sufficient funds"}), 500

        deployed_contract = None
        max_retries = 3

        try:
            update_deployment_status("Deploying contract...", "loading")

            for attempt in range(max_retries):
                try:
                    if data['type'] == "ERC721":
                        # Deploy ERC721 NFT contract
                        base_uri = data['baseUri']
                        if not base_uri.endswith('/'):
                            base_uri += '/'
                            
                        # Deploy contract with name, symbol, and base URI
                        deployed_contract = wallet.deploy_nft(
                            name=data['name'],
                            symbol=data['symbol'],
                            base_uri=base_uri
                        )
                        deployed_contract.wait()
                        print(f"ERC721 Contract deployed at: {deployed_contract.contract_address}")

                        # Mint initial NFT
                        mint_tx = wallet.invoke_contract(
                            contract_address=deployed_contract.contract_address,
                            method="mint",
                            args={"to": wallet.default_address.address_id}
                        )
                        mint_tx.wait()
                        print("Minted NFT with ID 1")

                    elif data['type'] == "ERC1155":
                        # Deploy ERC1155 Multi-token contract
                        base_uri = data['baseUri']
                        if not base_uri.endswith('/'):
                            base_uri += '/'
                            
                        # Deploy contract with base URI
                        deployed_contract = wallet.deploy_multi_token(uri=base_uri)
                        deployed_contract.wait()
                        print(f"ERC1155 Contract deployed at: {deployed_contract.contract_address}")

                        # Mint specified number of tokens
                        for token_id in range(1, data.get('tokenCount', 1) + 1):
                            mint_tx = wallet.invoke_contract(
                                contract_address=deployed_contract.contract_address,
                                method="mint",
                                args={
                                    "to": wallet.default_address.address_id,
                                    "id": str(token_id),
                                    "value": "1"
                                }
                            )
                            mint_tx.wait()
                            print(f"Minted token {token_id}")

                    break  # Exit retry loop if successful
                except Exception as e:
                    if attempt == max_retries - 1:
                        raise e
                    time.sleep(5)
                    continue

            # Return successful deployment response
            update_deployment_status("Minting tokens...", "complete")
            return jsonify({
                "success": True,
                "contract_address": deployed_contract.contract_address,
                "wallet_address": wallet.default_address.address_id,
                "wallet_balance": str(wallet.balance("eth")),
                "metadata_mapping": data.get('metadata', {})
            })

        except Exception as deploy_error:
            print(f"Deployment error: {str(deploy_error)}")
            traceback.print_exc()
            update_deployment_status("Deploying contract...", "error")
            return jsonify({
                "success": False, 
                "error": f"Contract deployment failed: {str(deploy_error)}"
            }), 500

    except Exception as e:
        print(f"Unexpected error: {str(e)}")
        traceback.print_exc()
        return jsonify({"success": False, "error": str(e)}), 500

# Run Flask app if executed directly
if __name__ == '__main__':
    app.run(host="0.0.0.0", port=5328, debug=True)

Step 3: Adding Pinata integration on the frontend

First, configure Pinata SDK in your frontend by creating a configuration file:

import { PinataSDK } from "pinata-web3";

interface PinataConfig {
  pinataJwt: string;
  pinataGateway: string;
}

const pinataJwt = process.env.NEXT_PUBLIC_PINATA_JWT;
const pinataGateway = process.env.NEXT_PUBLIC_PINATA_GATEWAY;

if (!pinataJwt || !pinataGateway) {
  throw new Error("Missing Pinata credentials in environment variables.");
}

export const PINATA_GATEWAY = pinataGateway;
export const pinata = new PinataSDK({
  pinataJwt,
  pinataGateway,
});

Next, implement the metadata upload functionality in your frontend component. The key steps are:

  1. Create metadata files for each token
  2. Upload files to IPFS using Pinata
  3. Construct proper token URIs
const handleMetadataUpload = async (tokens: TokenMetadata[]) => {
  // Create metadata files with proper numeric filenames
  const metadataFiles = tokens.map((token, index) => {
    const metadata = {
      name: token.name,
      description: token.description,
      image: token.image,
      attributes: JSON.parse(token.attributes),
    };

    const blob = new Blob([JSON.stringify(metadata, null, 2)], {
      type: "application/json",
    });

    // Remove .json extension as per requirements
    return new File([blob], `${index + 1}`, {
      type: "application/json",
    });
  });

  // Upload files as folder using fileArray
  const upload = await pinata.upload.fileArray(metadataFiles);

  if (!upload.IpfsHash) {
    throw new Error("Failed to get IPFS hash from upload");
  }

  // Construct base URI with ipfs:// protocol
  const baseUri = `ipfs://${upload.IpfsHash}/`;

  // Create metadata mapping for display purposes
  const metadataMapping: Record<string, TokenURIs> = {};
  tokens.forEach((_, index) => {
    const tokenId = index + 1;
    metadataMapping[tokenId] = {
      ipfs: `${baseUri}${tokenId}`,
      gateway: `https://${PINATA_GATEWAY}/ipfs/${upload.IpfsHash}/${tokenId}`
    };
  });

  return {
    baseUri,
    metadataMapping
  };
};

Find the complete implementation of the frontend upload component here.

Conclusion

The CDP SDK provides a powerful way to deploy and interact with NFT contracts on Base Sepolia. While this guide focuses on the core contract deployment functionality, the complete implementation in our Replit template shows how to integrate this with IPFS metadata storage and a user interface.

If you have any questions or need assistance, feel free to reach out to us in the CDP Discord.

This sample app is for demonstration purposes only. Make sure to secure your wallets and use testnet for testing to minimize risks.

Helpful Tips

  • Pinata Documentation: Refer to Pinata’s Documentation for details on configuring IPFS storage, setting up your API keys, and optimizing your metadata and file storage processes. Pinata also offers best practices for uploading and managing IPFS files.
  • NFT Metadata Standards: To ensure your NFT metadata is compatible across platforms, follow the OpenSea Metadata Standards. These standards cover essential metadata fields (name, description, image) and additional properties that will make your NFTs display correctly on marketplaces like OpenSea.
  • CDP Documentation: The CDP SDK Documentation provides a comprehensive guide on wallet and contract interactions. It also covers key aspects like invoke_contract for calling arbitrary contracts and read for retrieving state from smart contracts.
  • IPFS Gateway: For faster access to your IPFS-hosted files, use the Pinata Gateway or consider setting up your own IPFS gateway to reduce potential rate limiting and improve file retrieval times. This can be especially helpful for high-traffic applications