ERC1155: storing and retrieving IPFS hash using 'uri' method

I’m using the OpenZeppelin implementation of ERC1155 and want to explore the possibility of storing my metadata JSON file on IPFS. Unfortunately, the metadata will not be known ahead of time and needs to be added dynamically as part of the minting process.

I understand that it’s far more common to use a centralised metadata server (e.g. “https ://api.myserver.com/metadata/{id}.json”) but I’m keen to make my contract as decentralised as possible and IPFS seems to be the way to go for this.

This post by wighawag describes how it’s possible to override uri to return the IPFS hash for any given token but I’m struggling to implement this.

Here’s what I’ve got so far but I’m not sure that my approach is the best one, especially using the mint method’s data property in this way.

Any thoughts would be very much appreciated. Thanks!

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

import "@openzeppelin/contracts@4.2.0/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts@4.2.0/access/AccessControl.sol";
import "@openzeppelin/contracts@4.2.0/token/ERC1155/extensions/ERC1155Burnable.sol";

contract TestHashContract is ERC1155, AccessControl, ERC1155Burnable {
    bytes32 public constant URI_SETTER_ROLE = keccak256("URI_SETTER_ROLE");
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    
    // Mapping from token ID to IPFS hash
    mapping(uint256 => bytes) private _hashes;
    
    string private _uri;
    
    function _setURI(string memory newuri) internal virtual override {
        _uri = newuri;
    }
    
    function uri(uint256 id) public view virtual override returns (string memory) {
        //return _uri;
        return string(abi.encodePacked(_uri, _hashes[id]));
    }

    constructor() ERC1155("") {
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _setupRole(URI_SETTER_ROLE, msg.sender);
        _setupRole(MINTER_ROLE, msg.sender);
    }

    function setURI(string memory newuri) public {
        require(hasRole(URI_SETTER_ROLE, msg.sender));
        _setURI(newuri);
    }

    function mint(address account, uint256 id, uint256 amount, bytes memory data)
        public
    {
        require(hasRole(MINTER_ROLE, msg.sender));
        _hashes[id] = data;
        _mint(account, id, amount, data);
    }

    function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data)
        public
    {
        require(hasRole(MINTER_ROLE, msg.sender));
        _mintBatch(to, ids, amounts, data);
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC1155, AccessControl)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}