Node.js + Web3.jsを使用した機能検証用NFTミント、照会、移転、焼却

はじめに
--
本記事はLinux／Unixサーバーを対象にGUIを使用できない環境で機能検証するためにEthereumまたはEthereum互換の別チェーン（例：Polygon、Fantomなど）上にNFTミント、照会、移転、焼却の確認手順を解説します。ブロックチェーンの機能検証にお時間を多くかけたくない方におすすめです。

本記事では、Japan Open Chainという日本発のブロックチェーンをサンプルとして使用し、SolidityでERC721標準に基づいたNFTスマートコントラクトは既にデプロイした状態の確認手順となります。

※ Japan Open Chainについてはこちらの記事をご参考ください。
→ <a href="https://note.com/joc_official/n/n7c20014a5fa9">解説レポート〜「1分でわかる！Japan Open Chain」</a>

※ デプロイ済みのスマートコントラクトについてはこちらの記事をご参考ください。
→ <a href="https://qiita.com/k7sholder/items/e181356a4088eabe4fb2">hardhat + ether.jsを使用した機能検証用一番シンプルなNFT用コントラクト</a>

必要なソフトウェア：
・Linux／Unix環境（本記事ではUbuntuを使用する）
・Node.js／npm
・Ether.js
・fs.js
・readline.js
・web3.js
・秘密鍵（Metamaskなどからエクスポートしたものを使用）
・RPCサーバーURL (または別のETH互換チェーン)

※ 対象ブロックチェーンのRPCサーバーURLが不明な場合は下記サイトをご活用ください。
→ <a href="https://chainlist.org/"> [Chainlist] Helping users connect to EVM powered networks</a>

環境構築
--
手順1. Node.js と npm のインストール
--
まず、Node.jsとnpmがインストールされているか確認します。
```
which node
which npm
```
インストールされていない場合は、以下のコマンドでインストールします。
```
sudo apt update
sudo apt install nodejs
sudo apt install npm
```
手順2. 初期化
--
プロジェクトフォルダを作成して、初期化します。
```
mkdir simple-nft-test && cd simple-nft-test
npm init -y
```
手順3. 必要な依存関係のインストール
--
```
npm install --save ethers
npm install web3
npm install fs 
npm install readline
```

NFTミント
--
こちらのサンプルコードは実行された時のタイムスタンプを取得し、文字列化されたタイムスタンプをNFT IDとしてNFTをミントするためのものです。

xxx_nft_mint.jsというファイルと作成し、以下のスクリプトを記述します。
```joct_nft_mint.js
// 必要なパッケージをインポート
const { Web3 } = require('web3');
const fs = require('fs');

// ABIファイルのパスからABIを読み込む
const abi = JSON.parse(fs.readFileSync('/artifacts/contracts/SNFTS.sol/SNFTS.jsonの絶対パスに書換え').toString()).abi;

// プライベートキーとネットワークURL
const privateKey = "// 0x付きでウォレットの秘密鍵を挿入";  
//上記秘密鍵はスマートコントラクトオーナーの秘密鍵を使用

// Web3.js インスタンスを生成
const rpcURL = 'https://rpc-3.testnet.japanopenchain.org'; // 本サンプルはJOCのRPC URLを使用、JOC以外のブロックチェーンのRPCサーバーを使用する場合は書き換えてください
const web3 = new Web3(new Web3.providers.HttpProvider(rpcURL));

// コントラクトのアドレス
const contractAddress = "// 0x付きでコンタクトアドレスを挿入";

// アカウント情報のセットアップ
const account = web3.eth.accounts.privateKeyToAccount(privateKey);
web3.eth.accounts.wallet.add(account);

// コントラクトのインスタンスを作成
const nftContract = new web3.eth.Contract(abi, contractAddress);

// 現在のタイムスタンプをトークンIDとして使用
const generateTokenId = () => {
    // 秒単位のタイムスタンプ
    return Math.floor(Date.now() / 1000);  
};

// NFTをミントする関数
async function mintNFT(toAddress) {
    try {
        console.log("NFT mint start...");
        
        // トークンIDを動的に生成
        const tokenId = generateTokenId(); 

        // ミント用のトランザクションデータ
        const txData = nftContract.methods.nftMint(toAddress, tokenId).encodeABI();

        // トランザクションに必要なガス量を見積もる
        const gasEstimate = await nftContract.methods.nftMint(toAddress, tokenId).estimateGas({ from: account.address });

        // ガス価格を取得
        const gasPrice = await web3.eth.getGasPrice();

        // ミント用のトランザクション設定
        const tx = {
            from: account.address,
            to: contractAddress,
            // 見積もったガス量
            gas: gasEstimate,  
            // 現在のガス価格
            gasPrice: gasPrice,  
            data: txData,
        };

        // トランザクションに署名して送信
        const signedTx = await web3.eth.accounts.signTransaction(tx, privateKey);
        const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);

        console.log("NFT mint succeed!");
        console.log("Transaction hash:", receipt.transactionHash);
    } catch (error) {
        console.error("NFT mint failed:", error);
    }
}

// ミントするアドレス
const toAddress = "// 0x付きでウォレットアドレスを挿入"; 

// ミント実行
mintNFT(toAddress);
```
記述完了後、以下のコマンドを実行します。
```
node joct_nft_mint.js　// 各自のjsファイル名に書き換えてください
```
下記内容が出力されたら、NFTミントは成功しました。
```
NFT mint succeed!
Transaction hash: 0x0000000000000000A
```
ミントしたNFTは各ブロックチェーンのエクスプローラで確認できます。

今回のサンプルNFTは下記JOCのエクスプローラでステータスを確認できます。
→ <a href="https://explorer.japanopenchain.org/">Japan Open Chain explorer</a>

また、秘密鍵やコントラクトアドレス、ウォレットアドレスをハードコーティングしたくない方はこちらのreadlineを使用したコードをご参考ください。

```joct_nft_mint_rl.js
// 必要なパッケージをインポート
const { Web3 } = require('web3');
const fs = require('fs');
const readline = require('readline');

// ABIファイルのパスからABIを読み込む
const abi = JSON.parse(fs.readFileSync('/artifacts/contracts/SNFTS.sol/SNFTS.jsonの絶対パスに書換え').toString()).abi;

// Web3.js インスタンスを生成
const rpcURL = 'https://rpc-3.testnet.japanopenchain.org'; // 本サンプルはJOCのRPC URLを使用、JOC以外のブロックチェーンのRPCサーバーを使用する場合は書き換えてください
const web3 = new Web3(new Web3.providers.HttpProvider(rpcURL));

// readlineインターフェースを作成
const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

// アカウント情報のセットアップ
async function setupAccount(privateKey) {
    const account = web3.eth.accounts.privateKeyToAccount(privateKey);
    web3.eth.accounts.wallet.add(account);
    return account;
}

// 現在のタイムスタンプをトークンIDとして使用
const generateTokenId = () => {
    return Math.floor(Date.now() / 1000);  
};

// NFTをミントする関数
async function mintNFT(privateKey, contractAddress, toAddress, account) {
    try {
        const nftContract = new web3.eth.Contract(abi, contractAddress);
        console.log("NFT mint start...");

        const tokenId = generateTokenId(); 
        const txData = nftContract.methods.nftMint(toAddress, tokenId).encodeABI();
        const gasEstimate = await nftContract.methods.nftMint(toAddress, tokenId).estimateGas({ from: account.address });
        const gasPrice = await web3.eth.getGasPrice();

        const tx = {
            from: account.address,
            to: contractAddress,
            gas: gasEstimate,
            gasPrice: gasPrice,
            data: txData,
        };

        const signedTx = await web3.eth.accounts.signTransaction(tx, privateKey);
        const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);

        console.log("NFT mint succeed!");
        console.log("Transaction hash:", receipt.transactionHash);
    } catch (error) {
        console.error("NFT mint failed:", error);
    }
}

// ユーザーに必要な情報を入力させる
rl.question('Please enter your private key: ', (privateKey) => {
    rl.question('Please enter your contract address: ', (contractAddress) => {
        rl.question('Please enter the recipient address: ', (toAddress) => {
            setupAccount(privateKey)
                .then(account => mintNFT(privateKey,contractAddress, toAddress, account))
                .catch(err => console.error('Error setting up account:', err))
                .finally(() => rl.close());
        });
    });
});
```

NFT照会
--
こちらのサンプルコードはNFT IDとコントラクトアドレスでNFT所有アドレスを確認するためのものです。

xxx_nft_inquiry.jsというファイルと作成し、以下のスクリプトを記述します。
```joct_nft_inquiry.js
// 必要なパッケージをインポート
const { Web3 } = require('web3');
const fs = require('fs');

// ABIファイルのパスからABIを読み込む
const abi = JSON.parse(fs.readFileSync('/artifacts/contracts/SNFTS.sol/SNFTS.jsonの絶対パスに書換え').toString()).abi;

// Web3.js インスタンスを生成
const rpcURL = 'https://rpc-3.testnet.japanopenchain.org'; // 本サンプルはJOCのRPC URLを使用、JOC以外のブロックチェーンのRPCサーバーを使用する場合は書き換えてください
const web3 = new Web3(new Web3.providers.HttpProvider(rpcURL));

// コントラクトのアドレス
const contractAddress = "// スマートコントラクトアドレスを挿入";

// コントラクトのインスタンスを作成
const nftContract = new web3.eth.Contract(abi, contractAddress);

// NFTトークンの所有者を照会する関数
async function getNFTOwner(tokenId) {
    try {
        console.log(`Inquiring NTF owner : ${tokenId}...`);

        // コントラクトのownerOf関数を呼び出して所有者を取得
        const owner = await nftContract.methods.ownerOf(tokenId).call();
        console.log(`Token id : ${tokenId} ,NFT owner : ${owner}`);
    } catch (error) {
        console.error("Error has occured :", error);
    }
}

// 照会したいトークンID
const tokenId = XXXXXXXX; 

// NFT情報を照会
getNFTOwner(tokenId);
```
記述完了後、以下のコマンドを実行します。
```
node joct_nft_inquiry.js　// 各自のjsファイル名に書き換えてください
```
下記内容が出力されたら、NFT所有アドレス照会は成功しました。
```
Token id : 1253451 ,NFT owner : 0x0000000000000000A
```

また、NFT IDやコントラクトアドレスなどをハードコーティングしたくない方はこちらのreadlineを使用したコードをご参考ください。
```joct_nft_inquiry_rl.js
// 必要なパッケージをインポート
const { Web3 } = require('web3');
const fs = require('fs');
const readline = require('readline');

// ABIファイルのパスからABIを読み込む
const abi = JSON.parse(fs.readFileSync('/artifacts/contracts/SNFTS.sol/SNFTS.jsonの絶対パスに書換え').toString()).abi;

// Web3.js インスタンスを生成
const rpcURL = 'https://rpc-3.testnet.japanopenchain.org'; // 本サンプルはJOCのRPC URLを使用、JOC以外のブロックチェーンのRPCサーバーを使用する場合は書き換えてください
const web3 = new Web3(new Web3.providers.HttpProvider(rpcURL));

// readlineインターフェースを作成
const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

// コントラクトのインスタンスを作成
let nftContract;

// NFTトークンの所有者を照会する関数
async function getNFTOwner(tokenId) {
    try {
        console.log(`Inquiring NFT owner: ${tokenId}...`);
        const owner = await nftContract.methods.ownerOf(tokenId).call();
        console.log(`Token ID: ${tokenId}, NFT Owner: ${owner}`);
    } catch (error) {
        console.error("Error has occurred:", error);
    }
}

// ユーザーに必要な情報を入力させる
rl.question('Please enter your contract address: ', (contractAddress) => {
    rl.question('Please enter the token ID: ', (tokenId) => {
        nftContract = new web3.eth.Contract(abi, contractAddress);
        getNFTOwner(tokenId)
            .then(() => rl.close())
            .catch(err => {
                console.error('Error querying NFT owner:', err);
                rl.close();
            });
    });
});

```

NFT移転
---
こちらのサンプルコードはNFTを所有アドレスから別のアドレスに移転するためのものです。

xxx_nft_transfer.jsというファイルと作成し、以下のスクリプトを記述します。
```joct_nft_transfer.js
// 必要なパッケージをインポート
const { Web3 } = require('web3');
const fs = require('fs');

// ABIファイルのパスからABIを読み込む
const abi = JSON.parse(fs.readFileSync('/artifacts/contracts/SNFTS.sol/SNFTS.jsonの絶対パスに書換え').toString()).abi;

// Web3.js インスタンスを生成
const rpcURL = 'https://rpc-3.testnet.japanopenchain.org'; // 本サンプルはJOCのRPC URLを使用、JOC以外のブロックチェーンのRPCサーバーを使用する場合は書き換えてください
const web3 = new Web3(new Web3.providers.HttpProvider(rpcURL));

// コントラクトのアドレス
const contractAddress = "// スマートコントラクトアドレスを挿入";

// プライベートキーとアカウント
// 元の所有者のプライベートキー
const privateKey = "// 0x付きでウォレットの秘密鍵を挿入";  
const account = web3.eth.accounts.privateKeyToAccount(privateKey);
web3.eth.accounts.wallet.add(account);

// コントラクトのインスタンスを作成
const nftContract = new web3.eth.Contract(abi, contractAddress);

// NFT所有者を変更する関数（NFTの移転）
async function transferNFT(fromAddress, toAddress, tokenId) {
    try {
        console.log(`トークンID ${tokenId} を ${fromAddress} から ${toAddress} に移転中...`);

        // トランザクションデータの生成 (safeTransferFromはERC721標準関数)
        const txData = nftContract.methods.safeTransferFrom(fromAddress, toAddress, tokenId).encodeABI();

        // ガスの見積もり
        const gasEstimate = await nftContract.methods.safeTransferFrom(fromAddress, toAddress, tokenId).estimateGas({ from: fromAddress });

        // ガス価格を取得
        const gasPrice = await web3.eth.getGasPrice();

        // トランザクション設定
        const tx = {
            from: fromAddress,
            to: contractAddress,
            gas: gasEstimate,  // 見積もったガス量
            gasPrice: gasPrice,  // ガス価格
            data: txData,
        };

        // トランザクションに署名して送信
        const signedTx = await web3.eth.accounts.signTransaction(tx, privateKey);
        const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);

        console.log("NFT所有者が変更されました！ トランザクションハッシュ:", receipt.transactionHash);
    } catch (error) {
        console.error("エラーが発生しました:", error);
    }
}

// 移転元のアドレス（NFTの現在の所有者）
const fromAddress = "// 0x付きでウォレットアドレスを挿入";

// 移転先のアドレス（新しい所有者）
const toAddress = "// 0x付きでウォレットアドレスを挿入";

// 移転するトークンID
const tokenId = XXXXXXXXXXXXXX;

// NFT所有者の変更を実行
transferNFT(fromAddress, toAddress, tokenId);
```
記述完了後、以下のコマンドを実行します。
```
node joct_nft_transfer.js　// 各自のjsファイル名に書き換えてください
```
下記内容が出力されたら、NFT所有アドレス移転は成功しました。
```
トークンID 1246456 を 0x0000000000A から 0x0000000000B に移転中...
NFT所有者が変更されました！ トランザクションハッシュ: 0x000000000000XX
```
NFT所有アドレス移転は下記JOCのエクスプローラでも確認できます。
→ <a href="https://explorer.japanopenchain.org/">Japan Open Chain explorer</a>

また、秘密鍵やコントラクトアドレス、ウォレットアドレスをハードコーティングしたくない方はこちらのreadlineを使用したコードをご参考ください。

```joct_nft_transfer_rl.js
// 必要なパッケージをインポート
const { Web3 } = require('web3');
const fs = require('fs');
const readline = require('readline');

// ABIファイルのパスからABIを読み込む
const abi = JSON.parse(fs.readFileSync('/artifacts/contracts/SNFTS.sol/SNFTS.jsonの絶対パスに書換え').toString()).abi;

// Web3.js インスタンスを生成
const rpcURL = 'https://rpc-3.testnet.japanopenchain.org'; // 本サンプルはJOCのRPC URLを使用、JOC以外のブロックチェーンのRPCサーバーを使用する場合は書き換えてください
const web3 = new Web3(new Web3.providers.HttpProvider(rpcURL));

// readlineインターフェースを作成
const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

// コントラクトのインスタンスを作成
let nftContract;

// アカウント情報のセットアップ
async function setupAccount(privateKey) {
    const account = web3.eth.accounts.privateKeyToAccount(privateKey);
    web3.eth.accounts.wallet.add(account);
    return account;
}

// NFT所有者を変更する関数（NFTの移転）
async function transferNFT(contractAddress, privateKey, fromAddress, toAddress, tokenId, account) {
    try {
        console.log(`トークンID ${tokenId} を ${fromAddress} から ${toAddress} に移転中...`);

        const txData = nftContract.methods.safeTransferFrom(fromAddress, toAddress, tokenId).encodeABI();
        const gasEstimate = await nftContract.methods.safeTransferFrom(fromAddress, toAddress, tokenId).estimateGas({ from: fromAddress });
        const gasPrice = await web3.eth.getGasPrice();

        const tx = {
            from: fromAddress,
            to: contractAddress,
            gas: gasEstimate,
            gasPrice: gasPrice,
            data: txData,
        };

        const signedTx = await web3.eth.accounts.signTransaction(tx, privateKey);
        const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);

        console.log("NFT所有者が変更されました！ トランザクションハッシュ:", receipt.transactionHash);
    } catch (error) {
        console.error("エラーが発生しました:", error);
    }
}

// ユーザーに必要な情報を入力させる
rl.question('Please enter your contract address: ', (contractAddress) => {
    rl.question('Please enter your private key: ', (privateKey) => {
        rl.question('Please enter the from address: ', (fromAddress) => {
            rl.question('Please enter the to address: ', (toAddress) => {
                rl.question('Please enter the token ID: ', (tokenId) => {
                    nftContract = new web3.eth.Contract(abi, contractAddress);
                    setupAccount(privateKey)
                        .then(account => transferNFT(contractAddress, privateKey, fromAddress, toAddress, tokenId, account))
                        .catch(err => console.error('Error setting up account:', err))
                        .finally(() => rl.close());
                });
            });
        });
    });
});

```

NFT焼却
---
こちらのサンプルコードはNFTを焼却するためのものです。

xxx_nft_burn.jsというファイルと作成し、以下のスクリプトを記述します。

```joct_nft_burn.js
// 必要なパッケージをインポート
const { Web3 } = require('web3');
const fs = require('fs');

// ABIファイルのパスからABIを読み込む
const abi = JSON.parse(fs.readFileSync('/artifacts/contracts/SNFTS.sol/SNFTS.jsonの絶対パスに書換え').toString()).abi;

// Web3.js インスタンスを生成
const rpcURL = 'https://rpc-3.testnet.japanopenchain.org'; // 本サンプルはJOCのRPC URLを使用、JOC以外のブロックチェーンのRPCサーバーを使用する場合は書き換えてください
const web3 = new Web3(new Web3.providers.HttpProvider(rpcURL));

// コントラクトのアドレス
const contractAddress = "// 0x付きでウォレットアドレスを挿入";

// プライベートキーとアカウント
// 所有者のプライベートキー
const privateKey = "// 0x付きでウォレットの秘密鍵を挿入"; 
const account = web3.eth.accounts.privateKeyToAccount(privateKey);
web3.eth.accounts.wallet.add(account);

// コントラクトのインスタンスを作成
const nftContract = new web3.eth.Contract(abi, contractAddress);

// NFTをバーンする関数
async function burnNFT(tokenId) {
    try {
        console.log(`Burning NFT now : ${tokenId}...`);

        // トランザクションデータの生成 (burn関数をコントラクトから呼び出し)
        const txData = nftContract.methods.burn(tokenId).encodeABI();

        // ガスの見積もり
        const gasEstimate = await nftContract.methods.burn(tokenId).estimateGas({ from: account.address });

        // ガス価格を取得
        const gasPrice = await web3.eth.getGasPrice();

        // トランザクション設定
        const tx = {
            from: account.address,
            to: contractAddress,
            gas: gasEstimate,  // 見積もったガス量
            gasPrice: gasPrice,  // ガス価格
            data: txData,
        };

        // トランザクションに署名して送信
        const signedTx = await web3.eth.accounts.signTransaction(tx, privateKey);
        const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);

        console.log("Burning NFT finished！");
        console.log("Transaction hash : ", receipt.transactionHash);
    } catch (error) {
        console.error("Error has occured :", error);
    }
}

// バーンするトークンID
const tokenId = XXXXXXXXXXXXX;

// NFTをバーン
burnNFT(tokenId);
```
記述完了後、以下のコマンドを実行します。
```
node joct_nft_burn.js　// 各自のjsファイル名に書き換えてください
```
下記内容が出力されたら、NFT焼却は成功しました。
```
Burning NFT now :1234566
Burning NFT finished！
Transaction hash : 0x0000000000asdfaff
```
下記JOCのエクスプローラでもNFT所有アドレスが0X0000000000に変更されていたことを確認できます。
→ <a href="https://explorer.japanopenchain.org/">Japan Open Chain explorer</a>

また、秘密鍵やコントラクトアドレス、ウォレットアドレスをハードコーティングしたくない方はこちらのreadlineを使用したコードをご参考ください。

```joct_nft_burn_rl.js
// 必要なパッケージをインポート
const { Web3 } = require('web3');
const fs = require('fs');
const readline = require('readline');

// ABIファイルのパスからABIを読み込む
const abi = JSON.parse(fs.readFileSync('/artifacts/contracts/SNFTS.sol/SNFTS.jsonの絶対パスに書換え').toString()).abi;

// Web3.js インスタンスを生成
const rpcURL = 'https://rpc-3.testnet.japanopenchain.org'; // 本サンプルはJOCのRPC URLを使用、JOC以外のブロックチェーンのRPCサーバーを使用する場合は書き換えてください
const web3 = new Web3(new Web3.providers.HttpProvider(rpcURL));

// readlineインターフェースを作成
const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

// コントラクトのインスタンスを作成
let nftContract;

// アカウント情報のセットアップ
async function setupAccount(privateKey) {
    const account = web3.eth.accounts.privateKeyToAccount(privateKey);
    web3.eth.accounts.wallet.add(account);
    return account;
}

// NFTをバーンする関数
async function burnNFT(contractAddress, privateKey, tokenId, account) {
    try {
        console.log(`Burning NFT now: ${tokenId}...`);

        const txData = nftContract.methods.burn(tokenId).encodeABI();
        const gasEstimate = await nftContract.methods.burn(tokenId).estimateGas({ from: account.address });
        const gasPrice = await web3.eth.getGasPrice();

        const tx = {
            from: account.address,
            to: contractAddress,
            gas: gasEstimate,
            gasPrice: gasPrice,
            data: txData,
        };

        const signedTx = await web3.eth.accounts.signTransaction(tx, privateKey);
        const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);

        console.log("Burning NFT finished!");
        console.log("Transaction hash:", receipt.transactionHash);
    } catch (error) {
        console.error("Error has occurred:", error);
    }
}

// ユーザーに必要な情報を入力させる
rl.question('Please enter your contract address: ', (contractAddress) => {
    rl.question('Please enter your private key: ', (privateKey) => {
        rl.question('Please enter the token ID to burn: ', (tokenId) => {
            nftContract = new web3.eth.Contract(abi, contractAddress);
            setupAccount(privateKey)
                .then(account => burnNFT(contractAddress, privateKey, tokenId, account))
                .catch(err => console.error('Error setting up account:', err))
                .finally(() => rl.close());
        });
    });
});
```

最後
--
本記事は対象ブロックチェーンの機能検証用NFTをターゲットにしたNFTミント、照会、移転、焼却のサンプルコードをご紹介させていただきました。機能検証にお時間が多くかけたくない方にはお役に立てれば幸いです。