IPFS Desktop で立ち上げたノードに対して JS でアップロード

IPFS とは

IPFS とは P2P ネットワーク上でハイパーメディアプロトコルで、コンテンツのハッシュがアドレスとして使われるのでアップロード内容が変化したら参照するアドレスにも変更が必要といったものになります。ブロックチェーンのストレージとしての相性が良いようでブロックチェーンの台帳側には IPFS のアドレス(CID)を書き込むことで、その時点のコンテンツの履歴を残せるようになっています。

また、IPFS で使われる p2pプロトコルlibp2pになっていまして IPFS のために作られたもののようです。

libp2p.io

今回は IPFS 学習のため IPFS Desktop で立ち上げたノードに対して JS でアップロードしてみました。 IPFS Desktop は以下で取得できます。

github.com

IPFS Desktop で実際に立ち上がるノードは go-ipfs のもので IPFS Desktop はその UI となっています。
GitHub - ipfs/go-ipfs: IPFS implementation in Go

また、自前でノードを立ち上げてネットワーク上でコンテンツを共有ではなくブラウザからのアップロードを試したかったので js-ipfs でipfs-http-clientを利用しています。
GitHub - ipfs/js-ipfs: IPFS implementation in JavaScript

実際に確認したソースは examples にあるhttp-client-bundle-webpackになります。
js-ipfs/examples/http-client-bundle-webpack at master · ipfs/js-ipfs · GitHub

IPFS Desktop をシングルノードで立ち上げる

動作確認にあたり、今回は他のノードに共有する必要はないので IPFS Desktop 経由でインストールされたgo-ipfsの設定を行います。 具体的には~/.ipfs/configファイルのBootstrapの設定を以下のように空にします。

  "Bootstrap": [
    "/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
    "/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa",
    "/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb",
    "/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt",
    "/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
    "/ip4/104.131.131.82/udp/4001/quic/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ"
  ],

 ↓

"Bootstrap": [],

これで IPFS Desktop の peers を見たら 0 で表示され、他のノードにつながっていないことが確認できるかと思います。

f:id:steavevaivai:20210809214338p:plain

IPFS の基本的な動作確認

基本的な確認としてファイルのアップロードをしてみます。アップロードはipfs addでアップロードした内容はipfs catで確認できます。以下は Windowsコマンドプロンプトで確認しています。

C:\Users\arite\OneDrive\Desktop>type test1.txt
"hello world"

C:\Users\arite\OneDrive\Desktop>ipfs add test1.txt
 16 B / 16 B [================================================================================================] 100.00%added QmXeKtRSz7SVKp8Qh6tXtv6whRF5WXWQPwsqA38houakhZ test1.txt
 16 B / 16 B [================================================================================================] 100.00%
C:\Users\arite\OneDrive\Desktop>
C:\Users\arite\OneDrive\Desktop>ipfs cat QmXeKtRSz7SVKp8Qh6tXtv6whRF5WXWQPwsqA38houakhZ
"hello world"

ipfs add実行時のQmXeKtRSz7SVKp8Qh6tXtv6whRF5WXWQPwsqA38houakhZは CID というものでアップロードした内容により一意に決まるハッシュになっています。そのため、同じ内容でファイル名が異なるものをアップロードした倍も CID は同じになります。

C:\Users\arite\OneDrive\Desktop>copy test1.txt test2.txt
        1 個のファイルをコピーしました。

C:\Users\arite\OneDrive\Desktop>ipfs add test2.txt
 16 B / 16 B [================================================================================================] 100.00%added QmXeKtRSz7SVKp8Qh6tXtv6whRF5WXWQPwsqA38houakhZ test2.txt
 16 B / 16 B [================================================================================================] 100.00%
C:\Users\arite\OneDrive\Desktop>

また、ipfs catでは CID を指定して内容が確認できています。

http アクセスの許可

デフォルトのまま立ち上がった IPFS のノードに対してipfs-http-clientでリクエストを投げると CORS などではじかれるので以下のコマンドを実行し設定変更します。設定変更後はノードを再起動します。

ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin "[\"*\"]"
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Credentials "[\"true\"]"

ipfs-http-client での文字列アップロード

今回はjs-ipfsの examples に含まれるhttp-client-bundle-webpackの確認を行いまして、対象のソースは以下になります。

'use strict'
const React = require('react')
const { create: ipfsClient } = require('ipfs-http-client')
const stringToUse = 'hello world from webpacked IPFS3'

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      addr: null,
      id: null,
      version: null,
      protocol_version: null,
      added_file_hash: null,
      added_file_contents: null
    }

    this.connect = this.connect.bind(this)
    this.multiaddr = React.createRef()
  }

  async connect() {
    const ipfs = ipfsClient(this.multiaddr.current.value)
    const id = await ipfs.id()

    this.setState({
      id: id.id,
      version: id.agentVersion,
      protocol_version: id.protocolVersion
    })

    const file = await ipfs.add(stringToUse)
    const hash = file.cid
    this.setState({ added_file_hash: hash.toString() })

    const source = ipfs.cat(hash)
    let contents = ''
    const decoder = new TextDecoder('utf-8')

    for await (const chunk of source) {
      contents += decoder.decode(chunk, {
        stream: true
      })
    }

    contents += decoder.decode()

    this.setState({ added_file_contents: contents })
  }

  render() {
    if (this.state.id) {
      return (
        <div style={{ textAlign: 'center' }}>
          <h1 id="info-header">Everything is working!</h1>
          <p>Your ID is <strong>{this.state.id}</strong></p>
          <p>Your IPFS version is <strong>{this.state.version}</strong></p>
          <p>Your IPFS protocol version is <strong>{this.state.protocol_version}</strong></p>
          <div>
            <div>
              Added a file! <br />
              {this.state.added_file_hash}
            </div>
            <div>
              Contents of this file: <br />
              {this.state.added_file_contents}
            </div>
          </div>
        </div>
      )
    }

    return (
      <div style={{ textAlign: 'center' }}>
        <h1 id="connect-header">Enter the multiaddr for an IPFS node HTTP API</h1>
        <form>
          <input id="connect-input" type="text" defaultValue="/ip4/127.0.0.1/tcp/5001" ref={this.multiaddr} />
          <input id="connect-submit" type="button" value="Connect" onClick={this.connect} />
        </form>
      </div>
    )

  }
}
module.exports = App

ここでは以下で IPFS の http クライアントを立ち上げていまして、ipfsClientAPI アドレスを渡していまして今回は/ip4/127.0.0.1/tcp/5001になります。これは~/.ipfs/configAddresses.APIで指定したものになります。

const ipfs = ipfsClient(this.multiaddr.current.value)

それから、以下でテキストをアップロードし帰ってきた CID を取得しています。

const file = await ipfs.add(stringToUse)
const hash = file.cid
this.setState({ added_file_hash: hash.toString() })

そして、CID をもとにコンテンツを取得しています。

const source = ipfs.cat(hash)
let contents = ''
const decoder = new TextDecoder('utf-8')
for await (const chunk of source) {
  contents += decoder.decode(chunk, {
    stream: true
  })
}
contents += decoder.decode()
this.setState({ added_file_contents: contents })

実際に実行すると以下のようにアップロードおよび取得がうまくいっていることが確認できます。 f:id:steavevaivai:20210809214737p:plain

このときのCIDはQmNiw4occBBHvkQVdtvAB391yv6wZmMnU7QhfcSYqqqAtWになるのでipfs catを使って実際にアップロードされていることが確認できます。

C:\Users\arite\OneDrive\Desktop>ipfs cat QmNiw4occBBHvkQVdtvAB391yv6wZmMnU7QhfcSYqqqAtW
hello world from webpacked IPFS

NFT での IPFS 利用について

最初の方でブロックチェーンに IPFS の CID を書き込むとしていましたが、Etherum ネットワークでのトークン発行に使われる OpenZeppelin ではERC721 コントラクトを使っていまして、以下では_safeMintにアドレスとカウント値を渡してNFTトークンを発行し_setTokenURIに対してカウント値とmetadataURI(IPFSゲートウェイURL + CID)を渡してCID情報をコントラクトに書き込んでいます。 docs.ipfs.io