🌟

「bundlr + Next.js」を使い、1つのウォレットから支払をし、Arweaveに保存する方法

2023/01/28に公開

はじめに

この記事では、「サーバーサイドで支払いを済ませて、ユーザーは保存ボタンを押すだけでarweaveにファイルを保存できる」というコードを実装します。 
その際に何度もbundlrの人に質問し、お世話になったため、折角なのでZennにまとめてみました。

実装後のイメージ

公式サイト

各サービスの詳しい内容は、公式サイトからご確認ください。
https://www.arweave.org/
https://bundlr.network/

使用した技術

まず、Arweaveに保存するためにBundlrというサービスを使用しました。
Bundlrは「簡単かつ高速にArweaveに保存するためのツールを提供」するサービスです。
(Doc: https://docs.bundlr.network/)

また、UiにはChakra-UI、フレームワークにはNext.js を使用しています。

基本設定

arweaveの実装の前に、基本の設定をしていきます。
下記の2つのサイトに従ってnext.jsのappを作成してください。

1.typescriptのnext-appを作成
2.chakraの設定

appができたら、「index.tsx」に必要最小限の見た目を作っていきます。

index.tsx
const Index = (): JSX.Element => {
  const [file, setFile] = useState<File | null>(null)
  const [fileData, setFileData] = useState<Buffer>()
  
  const handleFileChange = async (e: any) => {
  const reader = new FileReader(
  const file = e.target.files[0]
  setFile(file)
  if (file) {
    reader.onloadend = () => {
      if (reader.result) {
        console.log(reader.result
	// @ts-ignore
        setFileData(Buffer.from(reader.result))
      }
    }
    reader.readAsArrayBuffer(file)
    }
  console.log('file to upload:', file.type)
}
  
  return (
    <Box my={'5rem'} mx={'3rem'}>
      <Input type={'file'} placeholder={'upload image'} onChange={(e) => handleFileChange(e)} />
      <Button onClick={() => {console.log('upload')}>Upload to Arweave</Button>
    </Box>
  )
}

これで見た目のコードは完成です。

APIの作成

次に、2つのapiを作成していきます。
(pages/apiの中にファイルを作成してください。)

1. 支払いたいアカウントに接続するapi

serverSideSigning.ts
import Bundlr from '@bundlr-network/client/build/node'
import { NextApiRequest, NextApiResponse } from 'next'

const api = async (req: NextApiRequest, res: NextApiResponse): Promise<void> => {
  let serverBundlr: Bundlr
  const key = process.env.NEXT_PUBLIC_WALLET_KEY
  const message = 'sign this message to connect to Bundlr.Network'
  if (!key) console.log('Not found key')
  serverBundlr = new Bundlr('https://devnet.bundlr.network', 'matic', key, {
    providerUrl: 'https://rpc-mumbai.matic.today',
  })
  await serverBundlr.ready()

  const preSignedHash = Buffer.from(
    await serverBundlr.currencyConfig.sign(Buffer.from(message)),
  ).toString('hex')
  return res.send({ preSignedHash })
}
export default api

2. 保存したいデータに署名するapi

signOnServer.ts
import Bundlr from '@bundlr-network/client/build/node'
import { NextApiRequest, NextApiResponse } from 'next'

const api = async (req: NextApiRequest, res: NextApiResponse) => {
  const key = process.env.NEXT_PUBLIC_WALLET_KEY
  if (!key) console.log('Not found key')
  
  //今回は、テストネットに保存するためdevnetを使用
  const serverBundlr = new Bundlr('https://devnet.bundlr.network', 'matic', key, {
    providerUrl: 'https://rpc-mumbai.matic.today',
  })
  const clientData = Buffer.from(req.body.dataToSign, 'hex')
  const data = req.body.dataSize

  const priceAtomic = await serverBundlr.getPrice(data)
  const priceConvertedBalance = await serverBundlr.utils.unitConverter(priceAtomic)
  const walletAtomicBalance = await serverBundlr.getLoadedBalance()
  const walletConvertedBalance = serverBundlr.utils.unitConverter(walletAtomicBalance)

// nodeの中にあるmaticとアップロードしたいファイルの価格を比較
  if (walletConvertedBalance < priceConvertedBalance) {
    try {
    //もしmaticが足りない場合は、nodeに送金
      let response = await serverBundlr.fund(priceAtomic)
      console.log(`Funding successful txID=${response.id} amount funded=${response.quantity}`)
    } catch (e) {
      console.log('Error funding node ', e)
    }
  }

  let atomicBalance = await serverBundlr.getLoadedBalance()
  let convertedBalance = serverBundlr.utils.unitConverter(atomicBalance)
  console.log(`node balance (converted) = ${convertedBalance}`)

  try {
  //保存したいファイルに、指定したウォレットから署名
    const signedData = await serverBundlr.currencyConfig.sign(clientData)
    const signedDataEncoded = Buffer.from(signedData)
    res.status(200).json({ msg: 'ok', signeddata: signedDataEncoded })
  } catch (error) {
    console.log('serversigning error', error)
    res.status(405).json({ msg: error })
  }
}
export default api

APIを使うためのコードを作成

基本設定で作成したindexに、コードを追加していきます。

index.tsx
import { WebBundlr } from '@bundlr-network/client'
import { Box, Button, Input } from '@chakra-ui/react'
import { utils } from 'ethers'
import { useState } from 'react'

const Index = (): JSX.Element => {
  const [file, setFile] = useState<File | null>(null)
  const [fileData, setFileData] = useState<Buffer>()

  const handleUpload = async () => {
    const res_ = await fetch('./api/serverSideSigning')
    const data = await res_.json()
    const preSignedHash = Buffer.from(data.preSignedHash, 'hex')
    //サーバーで接続した情報を元に、providerのモックを作成
    const provider = {
      getSigner: () => {
        return {
          signMessage: () => {
            return preSignedHash
          },
        }
      },
    }
    
    //webBundlrに接続
    const bundlr = new WebBundlr('https://devnet.bundlr.network', 'matic', provider, {
      providerUrl: 'https://rpc-mumbai.matic.today',
    })
    await bundlr.ready()

    if (!file) return console.log('file is not found')
    const reader = new FileReader()
    reader.onload = async () => {
      if (reader.result) {
        setFileData(Buffer.from(reader.result as any))
      } else {
        console.log('Not founded')
      }
    }
    if (!fileData) return
    //tagを設定。ここでimage/pngを設定することで、web上で画像を確認できる。
    const imageTags = [{ name: "Content-Type", value: "image/png" }]
    //トランザクションの作成
    const transaction = await bundlr.createTransaction(fileData, { tags: imageTags })

    const price = await bundlr.utils.getPrice('matic', file.size)
    console.log(file.name, ' of ', file.size, ' will cost:', utils.formatEther(price.toString()))
   
    //sign
    const signatureData = Buffer.from(await transaction.getSignatureData())
    const resp = await fetch('./api/signDataOnServer', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        dataToSign: Buffer.from(signatureData).toString('hex'),
        dataSize: file.size,
      }),
    })
    const resp2 = await resp.json()
    const signed = Buffer.from(resp2.signeddata, 'hex')
    transaction.getRaw().set(signed, 2)
    await transaction.setSignature(signed)

  //Arweaveに保存
    const res = await transaction.upload()
    console.log({ status: true, txId: res.id })
    console.log(`upload to ==> https://arweave.net/${res.id}`)
  }
}

コードの実行

ローカルホストを立ち上げて、画像をアップロードしてみてください。

コンソールにこんな感じのURLが表示されていたら成功です!
https://arweave.net/Ixf1kIMOs3JR2AkXPhngYWqlxT5AXh06rVevU_K_Cjc

最後に

今回の実装は、親切なbundlrのdev部門の方のおかげで作成することができました。
なので、宣伝しておきます!(bundlrの中の人ではないです笑)
https://twitter.com/BundlrNetwork?ref_src=twsrc^google|twcamp^serp|twgr^author

最後まで読んでいただき、ありがとうございました!

Discussion