DEV Community

Roberto V - rovicher.eth
Roberto V - rovicher.eth

Posted on • Originally published at Medium

Server-side paid uploads to Arweave through Bundlr

How to upload server-paid content from clients to Arweave

If you are developing web3 dApps and considering a cheap reliable way to store your immutable content, you should consider Arweave network blockweave technology. Blockweave is the storage unit blocks that get linked to secure availability and integrity of information.

Arweave provides a permanent storing solution (permaweb) to developers, that is paid once and stored forever. It has a useful library call Arweave-Bundles, allowing L2 networks to manage uploading files from users to Arweave permaweb in a cheaper way because they collect several user transactions and bundle them into a unique one to be loaded in one shot.

Bundlr is an Arweave L2 network that scales Arweave and facilitates transactions as they accept transactions from multiple blockchains and take payment for storing not only on its native currency (the AR) but in several other popular coins (ETH, SOL, MATIC, and lots more see here).

I have seen several examples on how to use Arvweave and Bundlr API to upload content from client, and I have found that Bundlr is straight and simple to use. Being able to launch and sign Arweave transaction from the blockchain I am already using makes for a better user experience because there is not need for the user to sign on two wallets for the same flow.

As I am presently developing a dApp that connects to Polygon blockchain, I need to be able to simplify UI for users. Some of my use cases demand clients to upload content and pay themselves but others cases require that a third party pay for them. I have not found code samples on how to do this, but at Bundlr discord I found helpful assistance to get it, thanks to Bundlr’s engineer @JesseCruzWright

I have built a sample Next app that connects to Metamask on Polygon Test Network (Mumbai) that allows uploading to test the code before incorporating into my app. I am publishing this review of that code as I think could be useful for others looking for a working example, so let’s review the basic code.

For this App these libraries are required: Bundlr-network/client and ethers, (bignumber.js is used as to do some conversions too)

npm i @bundlr-network/client
npm i ethers
npm i bignumber.js

First, we need to have some Bundlr Objects at play, on Web Client and a remote bundler at the server. We will use Metamask provider to connect to Polygon and pass this provider to Bundlr at client.

We instantiate a Web Bundlr object that gets linked to our Polygon Metamask account just after connecting to Metamask:

// pages/index.js
....
const handleConnectMM = async () => {
    if (!window.ethereum) {
      alert("Metamask not found!");
      return;
    }
    await window.ethereum.enable();
    provider.current = new providers.Web3Provider(window.ethereum);
    await provider.current._ready();
    webBundlr.current = new WebBundlr(
      networkConfig.bundlrNetwork,
      networkConfig.currency,
      provider.current
    );
    await webBundlr.current.ready();
    const amountParsed = new BigNumber(funds).multipliedBy(
      webBundlr.current.currencyConfig.base[1]
    );
    let bal0 = await webBundlr.current.getLoadedBalance();
    let bal1 = utils.formatEther(bal0.toString());
    setBalance(bal1);
    setConnected(true);
    setAccount(webBundlr.current.address);
  };
Enter fullscreen mode Exit fullscreen mode

Notice I immediately get the funded balance for the account. In Bundlr, before you can upload content you must sent some coin to Bundlr Node to pay for any transaction. Whatever your account is coming from (Polygon, Etherem, etc.) you send to Bundlr Node your native currency and they accrue it in ther ledger on your Bundlr account and use it for payment to Arweave on your behalf. In our case I am doing this to show that in spite of the local account having a zero balance, it will be able to upload content through server payment.

So I mentioned the native currency is send to a Bundlr Node. This is denoted by the network you sign to and you can have different funds at different nodes. Presently there is three Bundlr nodes at dev, node1 and node2

Now we need a server proxy bundlr object that has the funded server account we will use. The following method calls an API route from Next server:

// pages/index.js 
....
export const getRemoteBundler = async() => {
    console.log('client presignedhash', networkConfig)
    const result = await fetch('/api/presignedhash',{method: 'GET'})
    const data = await result.json()
    const presignedHash = Buffer.from(data.presignedHash,'hex')
    console.log('Client presignedHash:', presignedHash)
    const provider = {
      getSigner: () => {
          return {
              signMessage: () => {
                  return presignedHash
              }
          }
      }
    }
    const bundlr = new WebBundlr(networkConfig.bundlrNetwork, networkConfig.currency, provider);
    await bundlr.ready()
    console.log('remote bundlr ready:', bundlr.address)
    return bundlr
};
....
pages/api/presignedhash.js
...
import { networkConfig } from "../../web3/web3config"
const signingMsj="sign this message to connect to Bundlr.Network"
export default async function handler(req, res) {
  // get the bundler instance from the key we use at the server
  const serverBundlr = new Bundlr(
          networkConfig.bundlrNetwork,
          networkConfig.currency,
          networkConfig.serverAccPK
  )
  await serverBundlr.ready()
  const presignedHash = Buffer.from(await serverBundlr.currencyConfig.sign(signingMsj)).toString("hex");
  console.log('Server presignedHash',presignedHash)
  console.log('serverBundlr',serverBundlr.currency)
  res.status(200).send( {presignedHash} )
}
Enter fullscreen mode Exit fullscreen mode

That we3config is just a custom object with properties that tell what node and what private account I am using. I have included defintion for the three mentioned nodes. This is for connecting to devnet:

/ web3/web3config.js
....
const web3ConfigDevNet = {
    currency: 'matic',
    serverAccPK : process.env.POLYGON_PVK_ACCOUNT,
    serverProviderLink: process.env.ALCHEMY_MUMBAI_RPC_URL,
    bundlrNetwork:'https://devnet.bundlr.network'
}
...
//change to we3ConfigDevNet we3ConfigNode2 to connect working network
//export const networkConfig = web3ConfigNode2
export const networkConfig = web3ConfigDevNet
Enter fullscreen mode Exit fullscreen mode

Notice at client we instantiate Bundlr object through WebBundlr constructor and passing a web3 provider, as oppose to server where we use the NODE.JS constructor Bundlr and the account private key.

Also take a look at this code at getRemoteBundler method:

const provider = {
      getSigner: () => {
          return {
              signMessage: () => {
                  return presignedHash
              }
          }
      }
    }
Enter fullscreen mode Exit fullscreen mode

This is a mock provider that is passed to our proxy bundlr (you will find it as remoteBundlr state var at the app) at the client. The presignedHash is a signed message by the server Bundlr that helps us to get the remote Bundlr. The string that gets signed is a hard code value, so do not modify it or you will get another account address at the client:

const signingMsj="sign this message to connect to Bundlr.Network"
Enter fullscreen mode Exit fullscreen mode

Now we can display balances of local and remote account:

We are ready to start uploading content from our client even if our local web3 account has a zero balance on Bundlr devnet node.

Important: The sample app will upload wherever file is requested, but you should implement a mechanism to discriminate clients. For example a white list account, or choose a different server account according to logged client, and reject those not fulfilling the requisite.

We allow user to input a file and call uploadData method at client. The sequence will be to create and send a transaction through the remoteBundlr containing the file and an a Tag object. This tag object gets associated and index our uploaded file, so you can always retrieved it through graphql searches (there is an arweave playground to try your searches).
Also notice, content uploaded to devnet.bundlr.network will not upload your tags (although the content will be available at http://arweave.net/{your_tx_id}

Once transaction is created, it must be sent to the server for the server bundlr to sign it. Do not worry, althoug the file could be in the mega size range, we only need to send the signature (a mere 48 bytes), that is obtained this way: const signatureData = Buffer.from(await transaction.getSignatureData())

If the server agreed to sign the request, it will return the signed signature that we will incorporate to the transaction and send to Bundlr Node const res = await transaction.upload()

The following code does this process, is ran between client and server api route serversigning:

utils/bundlr.js
...
export const uploadData = async (remoteBundlr, webBundlr, file, fileData) => {
// tags array defines label with tag to our uploading content
// Helps to retrieve information at www.arweave.net/graphql
  const tags = [
    // Recomemnded tags for a file upload
    {name: "Content-Type", value: file.type},
    {name: "File", value: file.name },
    {name: "App-Name", value: "my-bundlr-app"},
    {name: "App-version", value: "1.0" },
    // custom tags
    // for example as in metadata server account is poster, we need to set who is the original owner
    {name: "owner", value: webBundlr.current.address }
  ]  
  const price= await remoteBundlr.getPrice(file.size)
  console.log(file.name,' of ', file.size,' will cost:', utils.formatEther(price.toString()))
  const transaction = remoteBundlr.createTransaction(fileData, { tags })
  // get signature data
  const signatureData = Buffer.from(await transaction.getSignatureData());
  // get signature signed by the paying server
  try {
    const resp = await fetch("/api/serversigning", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ datatosign: Buffer.from(signatureData).toString("hex") }),
    });
    const resp2= await resp.json()
    const signed = Buffer.from(resp2.signeddata,"hex")
//  add signed signature to transaction
    transaction.setSignature(signed)
  const res = await transaction.upload();
  return {status:true, txid:res.data.id}
} catch (error) {
    console.log("Error", error);
    return {status:false}
  }
}
pages/api/serversigning.js
....
const clientData= Buffer.from(req.body.datatosign,'hex')
  //const datatoSign = Buffer.from(req.body.signaturedata, "hex");

  try {  
    const signedData = await serverBundlr.currencyConfig.sign(clientData) 
    const signedDataEncoded = Buffer.from (signedData)
    console.log('signedData:', signedData)
    res.status(200).json({ msg:'ok',signeddata:signedDataEncoded })
  } catch (error) {
    console.log('serversigning error', error)
    res.status(405).json({msg:error})
  }
Enter fullscreen mode Exit fullscreen mode

The tags object could be anything useful for your purposes, but there are some recommended tags that will simplify your life, remember that content uploaded is immutable, so better marked it somehow in case in the future we need to version it.

You will notice the transaction is created and sent with remoteBundlr object, not with our local bundler, I just wanted to make evident that the local account has not funds at Bundlr node.

Complete code can be found here. Once set in your local system (take a look at README file) connect your Metamask wallet , clic Initialize remote bundlr button and once confirmed, you can start uploading files then.

After clicking Initialize you can start uploading to Arweave permaweb

After clicking Initialize you can start uploading to Arweave permaweb

The app has a minimum error control so be warned some nasty screen error could arise if you do not follow the happy path 🙂️.

And remember that node2 provides free uploads for content < 100 kb, which come in handy when testing lots of transactions.

There is a funding option there that sends crypto from your local account blockchain to the Bundlr node; if you require to fund a server account, you can log in at client with that account, once funded, export private key and set that key on Server bundlr instance. Later you log at client with another no-funded account at client. I have found that 0.02 MATIC at Mumbai testnet does it to upload small files to devnet node (less than 1 MB).

Top comments (0)