Solidity – How to Properly Implement a ContractURI for On-Chain NFTs

contract-developmentnftopenseapolygonsolidity

I am trying to implement contract-level metadata for an NFT collection on OpenSea, However, if I do so with the following code, the contract's metadata won't update on the OpenSea collections' page, nor will it update the description of the contract on the tokens' pages.

The TokenURI is also stored on-chain and does update when given proper data. In my case, it'll be an SVG image along with some strings for information. (more below snippet)

The code is missing everything that isn't relevant.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;

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

import "./base64.sol";

contract Test is ERC721Enumerable, ERC721URIStorage, Ownable {
    using Counters for Counters.Counter;
    using Strings for uint256;

    string private _contractURI;

    constructor() ERC721("Test", "T") {}

    function svgToImageURI(string memory _source) public pure returns (string memory) {
        string memory baseURL = "data:image/svg+xml;base64,";
        string memory svgBase64Encoded = Base64.encode(bytes(string(abi.encodePacked(_source))));
        return string(abi.encodePacked(baseURL, svgBase64Encoded));
    }

    function formatTokenURI(string memory _imageURI, string memory _name, string memory _description, string memory _properties) public pure returns (string memory) {
        return string(
            abi.encodePacked(
                "data:application/json;base64,",
                Base64.encode(
                    bytes(
                        abi.encodePacked(
                            '{"name":"', _name,
                            '", "description": "', _description, '"',
                            ', "attributes": ', _properties,
                            ', "image":"', _imageURI, '"}'
                        )
                    )
                )
            )
        );
    }

    function setTokenURI(uint256 _tokenId, string memory _tokenURI) public onlyOwner() {
        _setTokenURI(_tokenId, _tokenURI);
        emit tokenChanged(_tokenId);
    }

    function setContractURI(string memory contractURI_) public onlyOwner() {
        _contractURI = string(abi.encodePacked(
            "data:application/json;base64,",
            Base64.encode(
                bytes(
                    abi.encodePacked(
                        contractURI_
                    )
                )
            )
        ));
    }

    function contractURI() public view returns (string memory) {
        return _contractURI;
    }
}

The input I give to the setContractURI function is a simple JSON snippet from the OpenSea website so that I'm sure I comply with the rules of it.

{
  "name": "OpenSea Creatures",
  "description": "OpenSea Creatures are adorable aquatic beings primarily for demonstrating what can be done using the OpenSea platform. Adopt one today to try out all the OpenSea buying, selling, and bidding feature set.",
  "image": "https://openseacreatures.io/image.png",
  "external_link": "https://openseacreatures.io",
  "seller_fee_basis_points": 100, 
  "fee_recipient": "0xA97F337c39cccE66adfeCB2BF99C1DdC54C2D721"
}

What am I doing wrong? I'm on Polygon Mumbai testnet.

Best Answer

It seems like my setContractURI function had one encodepacket too much. it looks like it is working now on OpenSea.

    function setContractURI(string memory contractURI_) public onlyOwner() {
        _contractURI = string(abi.encodePacked(
                "data:application/json;base64,",
                Base64.encode(
                    bytes(
                        contractURI_
                    )
                )
            ));
    }
Related Topic