「bundlr + Next.js」を使い、1つのウォレットから支払をし、Arweaveに保存する方法
はじめに
この記事では、「サーバーサイドで支払いを済ませて、ユーザーは保存ボタンを押すだけでarweaveにファイルを保存できる」というコードを実装します。
その際に何度もbundlrの人に質問し、お世話になったため、折角なのでZennにまとめてみました。
実装後のイメージ
公式サイト
各サービスの詳しい内容は、公式サイトからご確認ください。
使用した技術
まず、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」に必要最小限の見た目を作っていきます。
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
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
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に、コードを追加していきます。
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の中の人ではないです笑)
最後まで読んでいただき、ありがとうございました!
Discussion