Secure NFT Storage System Solidity, IPFS, JavaScript

👤 Sharing: AI
```solidity
pragma solidity ^0.8.0;

// SPDX-License-Identifier: MIT

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

// Contract to manage NFTs stored on IPFS
contract SecureNFT is ERC721, Ownable {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    // Base URI for NFT metadata (Points to IPFS gateway)
    string public baseURI;

    // Constructor:  Sets the name and symbol for the NFT collection.
    //  Also sets the initial Base URI.
    constructor(string memory _name, string memory _symbol, string memory _baseURI) ERC721(_name, _symbol) {
        setBaseURI(_baseURI); // Initialize the base URI.  Crucial for metadata retrieval.
    }


    // Mints a new NFT and stores the IPFS hash of the metadata.
    // `_ipfsHash` is the IPFS hash of the JSON metadata file for this NFT.
    //  This function is only callable by the contract owner.
    function safeMint(address to, string memory _ipfsHash) public onlyOwner returns (uint256) {
        _tokenIds.increment();

        uint256 newItemId = _tokenIds.current();
        _safeMint(to, newItemId);  // Mint the NFT.

        // Set the token URI using the provided IPFS hash.
        _setTokenURI(newItemId, string(abi.encodePacked(baseURI, _ipfsHash)));

        return newItemId;
    }


    // _setTokenURI is deprecated in newer versions of ERC721.  For compatibility,
    // and to be explicit, we override it.  The _setTokenURI function from the ERC721 contract
    // automatically handles the token URI.
    function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal virtual override {
        super._setTokenURI(tokenId, _tokenURI);
    }


    // Sets the base URI for all NFTs in this collection.
    //  The base URI is a prefix for the IPFS hash of each NFT's metadata.
    //  This allows changing the IPFS gateway being used without redeploying the contract.
    function setBaseURI(string memory _newBaseURI) public onlyOwner {
        baseURI = _newBaseURI;
    }


    // Returns the base URI.
    function getBaseURI() public view returns (string memory) {
        return baseURI;
    }

    // Override baseURI() to return baseURI
    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

        string memory _tokenURI = super.tokenURI(tokenId);
        return _tokenURI;
    }

}
```

```javascript
// JavaScript (Node.js) Example for interacting with the SecureNFT contract

const ethers = require('ethers');
const axios = require('axios'); // For uploading metadata to IPFS

// 1. Set up your Ethereum provider and signer
//    (replace with your actual provider URL and private key)
const providerUrl = 'http://localhost:8545'; // Or your Infura/Alchemy URL
const privateKey = '0x...'; // Your private key (DO NOT COMMIT!)

const provider = new ethers.providers.JsonRpcProvider(providerUrl);
const wallet = new ethers.Wallet(privateKey, provider);

// 2. Contract Address and ABI
//    (replace with your actual contract address and ABI)
const contractAddress = '0x...'; // Your deployed contract address
const contractABI = [
  // Paste your contract ABI here (from Remix or Hardhat output)
  // Example (replace with your actual ABI):
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "_name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "_symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "_baseURI",
        "type": "string"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "string",
        "name": "_ipfsHash",
        "type": "string"
      }
    ],
    "name": "safeMint",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "baseURI",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },

  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "tokenURI",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
];

const contract = new ethers.Contract(contractAddress, contractABI, wallet);

// 3. IPFS Configuration
//    (replace with your preferred IPFS service)
const ipfsGateway = 'https://ipfs.io/ipfs/'; // Public IPFS gateway (consider using your own)
const ipfsApiUrl = 'https://ipfs.infura.io:5001/api/v0/add';  // Replace with your IPFS API endpoint (Infura, Pinata, etc.)  This example uses Infura.  You will need an Infura project ID and API key.

// Function to upload JSON metadata to IPFS
async function uploadMetadataToIPFS(metadata) {
  try {
    const response = await axios.post(ipfsApiUrl, metadata, {
      headers: {
        'Content-Type': 'application/json',
        // Add your Infura project ID and API key for authentication if needed.
        //'Authorization': 'Basic ' + Buffer.from('YOUR_INFURA_PROJECT_ID:YOUR_INFURA_API_KEY').toString('base64')
      },
    });
    return response.data.Hash; // Returns the IPFS hash of the uploaded file
  } catch (error) {
    console.error('Error uploading to IPFS:', error);
    throw error;
  }
}


// 4. Main Function
async function main() {
  try {
    // a. Define the NFT metadata
    const nftMetadata = {
      name: 'My Awesome NFT',
      description: 'This is a description of my awesome NFT.',
      image: 'https://example.com/image.png', // Replace with your image URL
      attributes: [
        { trait_type: 'Rarity', value: 'Rare' },
        { trait_type: 'Level', value: 10 },
      ],
    };

    // b. Upload metadata to IPFS
    const ipfsHash = await uploadMetadataToIPFS(nftMetadata);
    console.log('IPFS Hash:', ipfsHash);

    // c. Mint the NFT (call the safeMint function on the contract)
    const recipientAddress = wallet.address; // The address to mint the NFT to
    const tx = await contract.safeMint(recipientAddress, ipfsHash);

    console.log('Minting transaction sent:', tx.hash);
    await tx.wait(); // Wait for the transaction to be mined
    console.log('NFT Minted!');

    // d. Get the token URI
    const tokenId = await contract.totalSupply(); //Assumes the last minted token is the total supply
    const tokenURI = await contract.tokenURI(tokenId);
    console.log('Token URI:', tokenURI); // This should be your IPFS link

    // You can then use the `tokenURI` to retrieve the metadata from IPFS

  } catch (error) {
    console.error('Error:', error);
  }
}

main();
```

```html
<!DOCTYPE html>
<html>
<head>
  <title>Secure NFT Display</title>
  <style>
    body { font-family: sans-serif; }
    .nft-container {
      border: 1px solid #ccc;
      padding: 10px;
      margin: 10px;
      width: 300px;
    }
    .nft-image { width: 100%; height: auto; }
  </style>
</head>
<body>

  <h1>NFT Display</h1>

  <div id="nftDisplay">
    <!-- NFT data will be injected here -->
  </div>

  <script>
    async function displayNFT() {
      // Replace with your contract address, token ID, and IPFS gateway
      const contractAddress = '0x...'; // Your contract address
      const tokenId = 1;             // The token ID of the NFT you want to display
      const ipfsGateway = 'https://ipfs.io/ipfs/'; // Your IPFS gateway

      // Replace with your actual contract ABI snippet for `tokenURI` function only
      const contractABI = [
          {
            "inputs": [
              {
                "internalType": "uint256",
                "name": "tokenId",
                "type": "uint256"
              }
            ],
            "name": "tokenURI",
            "outputs": [
              {
                "internalType": "string",
                "name": "",
                "type": "string"
              }
            ],
            "stateMutability": "view",
            "type": "function"
          }
      ];

      // Check if MetaMask is installed
      if (typeof window.ethereum === 'undefined') {
        document.getElementById('nftDisplay').innerText = 'Please install MetaMask!';
        return;
      }

      // Create a provider using MetaMask
      const provider = new ethers.providers.Web3Provider(window.ethereum);

      // Create a contract instance
      const contract = new ethers.Contract(contractAddress, contractABI, provider);

      try {
        // Get the token URI from the contract
        const tokenURI = await contract.tokenURI(tokenId);

        // Fetch the NFT metadata from IPFS
        const response = await fetch(ipfsGateway + tokenURI.replace('ipfs://', '')); //Replace to work with web3provider
        const metadata = await response.json();

        // Create the HTML to display the NFT
        let nftHTML = `
          <div class="nft-container">
            <h2>${metadata.name}</h2>
            <img src="${metadata.image}" alt="${metadata.name}" class="nft-image">
            <p>${metadata.description}</p>
            <p><b>Rarity:</b> ${metadata.attributes.find(attr => attr.trait_type === 'Rarity')?.value || 'N/A'}</p>
            <p><b>Level:</b> ${metadata.attributes.find(attr => attr.trait_type === 'Level')?.value || 'N/A'}</p>
          </div>
        `;

        // Inject the HTML into the page
        document.getElementById('nftDisplay').innerHTML = nftHTML;

      } catch (error) {
        console.error('Error fetching NFT data:', error);
        document.getElementById('nftDisplay').innerText = 'Error fetching NFT data.  Check console.';
      }
    }

    // Call the function to display the NFT
    displayNFT();
  </script>

  <script src="https://cdn.ethers.io/lib/ethers-5.4.umd.min.js"
        type="application/javascript"></script>

</body>
</html>
```

**Explanation:**

**Solidity Contract (SecureNFT.sol):**

1.  **`pragma solidity ^0.8.0;`**:  Specifies the Solidity compiler version.
2.  **`import ...;`**: Imports OpenZeppelin contracts for ERC721 token standard, counters, and ownership management.  OpenZeppelin provides secure and well-tested implementations of common smart contract patterns.
3.  **`contract SecureNFT is ERC721, Ownable`**: Defines the contract `SecureNFT`, inheriting from ERC721 (for NFT functionality) and Ownable (for restricting access to certain functions to the contract owner).
4.  **`using Counters for Counters.Counter;`**: Enables the use of the `Counters` library to generate unique token IDs.
5.  **`Counters.Counter private _tokenIds;`**: A counter to track the next available token ID.
6.  **`string public baseURI;`**:  A string variable to store the base URI of the NFT metadata.  This allows you to easily change the IPFS gateway being used without needing to redeploy the contract.
7.  **`constructor(string memory _name, string memory _symbol, string memory _baseURI) ERC721(_name, _symbol) { ... }`**:  The constructor initializes the contract.  It takes the name and symbol of the NFT collection as input and passes them to the ERC721 constructor.  It also sets the initial base URI.
8.  **`safeMint(address to, string memory _ipfsHash) public onlyOwner returns (uint256)`**:  This function mints a new NFT and assigns it to the specified address (`to`).
    *   `onlyOwner` modifier ensures that only the contract owner can call this function.
    *   `_ipfsHash`: This argument takes the IPFS hash of the JSON metadata for the NFT.
    *   The function increments the token ID counter, mints the NFT using `_safeMint` (from ERC721), and sets the token URI using `_setTokenURI`.
    *   It returns the newly minted token ID.
9.  **`_setTokenURI(uint256 tokenId, string memory _tokenURI) internal virtual override { ... }`**: This overrides the `_setTokenURI` function from ERC721.  It calls the parent function. It's crucial to ensure the token URI is correctly set.
10. **`setBaseURI(string memory _newBaseURI) public onlyOwner { ... }`**: Allows the contract owner to update the base URI.
11. **`getBaseURI() public view returns (string memory) { ... }`**:  A getter function to retrieve the current base URI.
12. **`tokenURI(uint256 tokenId) public view virtual override returns (string memory)`**: Overrides the `tokenURI` function from ERC721 to correctly construct the full token URI by combining the base URI and the IPFS hash.  It ensures that a token exists before attempting to retrieve its URI.

**JavaScript (Node.js):**

1.  **Dependencies:** Uses `ethers` for interacting with the Ethereum blockchain and `axios` for making HTTP requests (to upload metadata to IPFS).  You'll need to install these using `npm install ethers axios`.
2.  **Provider and Signer:** Sets up an Ethereum provider (using `ethers.providers.JsonRpcProvider`) and a signer (using `ethers.Wallet`).  **Important:**  Replace `providerUrl` and `privateKey` with your actual values.  **Never commit your private key to a public repository!** Use environment variables for sensitive information.
3.  **Contract Instance:** Creates an `ethers.Contract` instance, which allows you to interact with the deployed smart contract.  You need to provide the contract address and ABI (Application Binary Interface).  The ABI describes the contract's functions and how to call them.
4.  **IPFS Configuration:**  Defines the IPFS gateway and API URL.  You'll likely want to use your own IPFS service (like Infura, Pinata, or a self-hosted IPFS node) for reliable storage and retrieval.  The example uses Infura, so you'll need to provide your project ID and API key if you want to use that.
5.  **`uploadMetadataToIPFS(metadata)`**:  This function takes the NFT metadata (a JSON object) as input, uploads it to IPFS using the `axios` library, and returns the IPFS hash of the uploaded metadata.  It includes authentication headers if you're using a service like Infura.
6.  **`main()`**:  The main function performs the following steps:
    *   Defines the NFT metadata (name, description, image, attributes).
    *   Calls `uploadMetadataToIPFS()` to upload the metadata to IPFS and get the IPFS hash.
    *   Calls the `safeMint()` function on the smart contract, passing in the recipient address and the IPFS hash.
    *   Waits for the minting transaction to be confirmed.
    *   Retrieves the token URI from the contract using `tokenURI()`.
    *   Logs the IPFS hash and the token URI to the console.
7.  **Error Handling:**  Includes `try...catch` blocks to handle potential errors during the process.

**HTML/JavaScript (Front-end):**

1.  **HTML Structure:** Sets up a basic HTML page with a `div` element where the NFT data will be displayed.
2.  **`displayNFT()` Function:** This function handles the process of retrieving NFT data from the smart contract and displaying it on the page.
3.  **Web3 Provider (MetaMask):** It checks if MetaMask is installed and uses `window.ethereum` to create a Web3 provider.
4.  **Contract Instance:**  Similar to the Node.js example, it creates an `ethers.Contract` instance using the contract address and ABI.  **Important:** In this front-end example, the ABI only needs to contain the `tokenURI` function definition.
5.  **`tokenURI()` Call:** It calls the `tokenURI(tokenId)` function on the contract to get the IPFS URI of the NFT metadata.
6.  **Fetching Metadata from IPFS:** It fetches the JSON metadata from IPFS using the `fetch()` API.
7.  **Displaying NFT Data:** It constructs HTML elements to display the NFT's name, image, description, and attributes, and injects them into the `nftDisplay` div.
8.  **Error Handling:** Includes a `try...catch` block to handle potential errors.
9.  **Ethers.js Library:** The HTML includes a script tag to load the Ethers.js library from a CDN.

**Key Improvements and Considerations:**

*   **Security:** Using OpenZeppelin contracts significantly improves the security of your smart contract.
*   **Flexibility:** The `baseURI` variable makes it easy to switch IPFS gateways without redeploying the contract.
*   **IPFS Integration:**  The code clearly demonstrates how to upload metadata to IPFS and store the IPFS hash on the blockchain.
*   **Error Handling:**  The examples include basic error handling.  You should add more robust error handling for production environments.
*   **User Interface:** The front-end example provides a basic way to display NFT data.  You'll likely want to create a more sophisticated UI for a real application.
*   **IPFS Service:**  Choose a reliable IPFS service (Infura, Pinata, etc.) or run your own IPFS node to ensure the availability of your NFT metadata.
*   **Authentication:**  If you are using a private IPFS service (like Infura or Pinata), make sure to include the appropriate authentication headers in your IPFS requests.
*   **MetaMask Integration:** The front-end code requires MetaMask or a similar Web3 provider to interact with the blockchain.
*   **Gas Optimization:**  Consider gas optimization techniques for your Solidity contract to reduce transaction costs.
*   **Event Emission:**  Emit events in your smart contract when NFTs are minted or transferred. This allows you to easily track NFT activity off-chain.
*   **Centralized vs. Decentralized:**  Be aware that while the NFT itself is stored on the blockchain, the metadata (image, description, etc.) is stored off-chain on IPFS.  This introduces a degree of centralization.
*   **IPFS Persistence:**  Pin your NFT metadata to ensure that it remains available on IPFS.
*   **Off-chain Metadata Updates:** The current design doesn't allow for updating the metadata associated with an NFT after it's minted.  Consider adding functionality to update metadata if needed (although this introduces complexity and potential security risks). You would likely use a separate system (like a database) linked to the smart contract to manage mutable metadata.
*   **Testing:** Write thorough unit tests for your smart contract.

This comprehensive example provides a solid foundation for building a secure and functional NFT storage system using Solidity, IPFS, and JavaScript.  Remember to adapt the code to your specific requirements and security best practices.
👁️ Viewed: 10

Comments