NFTによるデジタル文化財の表現

デジタル文化財をブロックチェーン上で管理するために、ERC721規格のNFT(Non-Fungible Token)を使用します。各文化財は一意のトークンとして発行され、その所有権・メタデータ・画像データがブロックチェーンとIPFS上に記録されます。

本章では、以下の3つのステップでNFT対応を実装します。

  1. Solidityによるスマートコントラクトの作成
  2. Pinata IPFSを使用したメタデータと画像の保存
  3. ミンティング(発行)フローの実装

ERC721コントラクトの実装

OpenZeppelinのERC721をベースに、文化財管理に特化したコントラクトを作成します。

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

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

contract CulturalHeritageNFT is ERC721, ERC721URIStorage, Ownable {
    uint256 private _nextTokenId;

    // 文化財の基本情報
    struct CulturalItem {
        string name;           // 文化財名
        string description;    // 説明
        string institution;    // 所蔵機関
        string dateCreated;    // 制作年代
        uint256 mintedAt;      // NFT発行日時
    }

    // tokenId => CulturalItem
    mapping(uint256 => CulturalItem) public culturalItems;

    event CulturalItemMinted(
        uint256 indexed tokenId,
        address indexed owner,
        string name,
        string institution,
        uint256 timestamp
    );

    constructor()
        ERC721("CulturalHeritageNFT", "CHNFT")
        Ownable(msg.sender)
    {}

    /// @notice 文化財NFTを発行する
    /// @param to 発行先アドレス
    /// @param uri IPFS上のメタデータURI
    /// @param name 文化財名
    /// @param description 説明
    /// @param institution 所蔵機関
    /// @param dateCreated 制作年代
    function mintCulturalItem(
        address to,
        string memory uri,
        string memory name,
        string memory description,
        string memory institution,
        string memory dateCreated
    ) public onlyOwner returns (uint256) {
        uint256 tokenId = _nextTokenId++;

        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);

        culturalItems[tokenId] = CulturalItem({
            name: name,
            description: description,
            institution: institution,
            dateCreated: dateCreated,
            mintedAt: block.timestamp
        });

        emit CulturalItemMinted(
            tokenId,
            to,
            name,
            institution,
            block.timestamp
        );

        return tokenId;
    }

    /// @notice 文化財情報を取得する
    function getCulturalItem(uint256 tokenId)
        public view returns (CulturalItem memory)
    {
        require(tokenId < _nextTokenId, "Token does not exist");
        return culturalItems[tokenId];
    }

    // 必須のオーバーライド
    function tokenURI(uint256 tokenId)
        public view override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

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

このコントラクトの特徴は、標準的なERC721に加えて CulturalItem 構造体を持つ点です。文化財名、説明、所蔵機関、制作年代といった文化財固有の情報をオンチェーンに記録します。

Pinata IPFSによるメタデータの保存

NFTのメタデータと画像データは、Pinata IPFSに保存します。Pinataは、IPFSへのピンニング(永続化)サービスを提供しており、V3 APIを使用することで効率的にファイルをアップロードできます。

画像のアップロード

まず、文化財の画像をIPFSにアップロードします。

import PinataSDK from "@pinata/sdk";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: process.env.PINATA_GATEWAY!,
});

// 画像ファイルをIPFSにアップロード
async function uploadImage(filePath: string): Promise<string> {
  const readableStream = fs.createReadStream(filePath);

  const result = await pinata.pinFileToIPFS(readableStream, {
    pinataMetadata: {
      name: "cultural-heritage-image",
    },
    pinataOptions: {
      cidVersion: 1,
    },
  });

  console.log("画像アップロード完了:", result.IpfsHash);
  return `ipfs://${result.IpfsHash}`;
}

メタデータJSONの作成とアップロード

NFTのメタデータは、ERC721のメタデータ標準に準拠したJSON形式で作成します。

interface NFTMetadata {
  name: string;
  description: string;
  image: string; // IPFS URI
  attributes: {
    trait_type: string;
    value: string;
  }[];
}

async function uploadMetadata(
  name: string,
  description: string,
  imageUri: string,
  institution: string,
  dateCreated: string
): Promise<string> {
  const metadata: NFTMetadata = {
    name,
    description,
    image: imageUri,
    attributes: [
      { trait_type: "Institution", value: institution },
      { trait_type: "Date Created", value: dateCreated },
      { trait_type: "Type", value: "Cultural Heritage" },
      { trait_type: "Standard", value: "ERC721" },
    ],
  };

  const result = await pinata.pinJSONToIPFS(metadata, {
    pinataMetadata: {
      name: `metadata-${name}`,
    },
  });

  console.log("メタデータアップロード完了:", result.IpfsHash);
  return `ipfs://${result.IpfsHash}`;
}

文化財固有の属性(所蔵機関、制作年代など)は attributes 配列に格納します。これにより、OpenSeaなどのNFTマーケットプレイスでも属性情報を表示できます。

ミンティングフローの実装

画像とメタデータのアップロードが完了したら、スマートコントラクトのミント関数を呼び出してNFTを発行します。

import { ethers } from "ethers";

async function mintCulturalHeritage(
  name: string,
  description: string,
  imagePath: string,
  institution: string,
  dateCreated: string
) {
  // 1. 画像をIPFSにアップロード
  const imageUri = await uploadImage(imagePath);
  console.log("画像URI:", imageUri);

  // 2. メタデータを作成してIPFSにアップロード
  const metadataUri = await uploadMetadata(
    name,
    description,
    imageUri,
    institution,
    dateCreated
  );
  console.log("メタデータURI:", metadataUri);

  // 3. スマートコントラクトでNFTをミント
  const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
  const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);

  const contract = new ethers.Contract(
    process.env.CONTRACT_ADDRESS!,
    CulturalHeritageNFT.abi,
    signer
  );

  const tx = await contract.mintCulturalItem(
    signer.address,
    metadataUri,
    name,
    description,
    institution,
    dateCreated
  );

  const receipt = await tx.wait();
  console.log("ミント完了! トランザクション:", receipt.hash);

  // イベントからtokenIdを取得
  const event = receipt.logs.find(
    (log: any) => log.fragment?.name === "CulturalItemMinted"
  );

  if (event) {
    const tokenId = event.args[0];
    console.log("トークンID:", tokenId.toString());
  }
}

デプロイスクリプト

コントラクトをSepoliaテストネットにデプロイするスクリプトも作成します。

// scripts/deploy.ts
import { ethers } from "hardhat";

async function main() {
  const CulturalHeritageNFT = await ethers.getContractFactory(
    "CulturalHeritageNFT"
  );
  const contract = await CulturalHeritageNFT.deploy();
  await contract.waitForDeployment();

  const address = await contract.getAddress();
  console.log("CulturalHeritageNFT deployed to:", address);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
# ローカルネットワークでのデプロイ
npx hardhat run scripts/deploy.ts

# Sepoliaテストネットへのデプロイ
npx hardhat run scripts/deploy.ts --network sepolia

テストの作成

コントラクトの動作を検証するテストを記述します。

// test/CulturalHeritageNFT.test.ts
import { expect } from "chai";
import { ethers } from "hardhat";

describe("CulturalHeritageNFT", function () {
  it("文化財NFTをミントできる", async function () {
    const [owner] = await ethers.getSigners();

    const Contract = await ethers.getContractFactory("CulturalHeritageNFT");
    const contract = await Contract.deploy();

    const tx = await contract.mintCulturalItem(
      owner.address,
      "ipfs://QmTest123",
      "源氏物語絵巻",
      "平安時代後期の絵巻物",
      "徳川美術館",
      "12世紀"
    );

    await tx.wait();

    // トークンが発行されたことを確認
    expect(await contract.ownerOf(0)).to.equal(owner.address);

    // 文化財情報を確認
    const item = await contract.getCulturalItem(0);
    expect(item.name).to.equal("源氏物語絵巻");
    expect(item.institution).to.equal("徳川美術館");
  });

  it("メタデータURIが正しく設定される", async function () {
    const [owner] = await ethers.getSigners();

    const Contract = await ethers.getContractFactory("CulturalHeritageNFT");
    const contract = await Contract.deploy();

    await contract.mintCulturalItem(
      owner.address,
      "ipfs://QmMetadataHash",
      "鳥獣戯画",
      "国宝の絵巻物",
      "高山寺",
      "12-13世紀"
    );

    expect(await contract.tokenURI(0)).to.equal("ipfs://QmMetadataHash");
  });
});
# テストの実行
npx hardhat test

まとめ

本章では、ERC721規格を使用してデジタル文化財をNFTとして表現するスマートコントラクトを作成しました。Pinata IPFSを利用して画像とメタデータを分散的に保存し、ブロックチェーン上にはトークンの所有権と文化財の基本情報を記録する仕組みを実装しました。

次章では、文化財の来歴情報(修復履歴、展示履歴など)を追跡する機能を追加していきます。

関連記事