Uploading Files to Arweave with Irys

Uploading Files to Arweave with Irys

Arweave allows for decentralized data storage. Users pay a one-time fee to upload their data and are guaranteed permanent storage. Miners ensure data permanence by storing and replicating data in exchange for Arweave's native AR token. You can think of Arweave as a global hard disk or a decentralized Amazon S3.

This article explains how to upload files to Arweave using IRYS. You will learn what Irys is and how it makes data upload to Arweave seamless. You will create a simple NextJS application to upload and view the images uploaded to Arweave, so sit back, relax, and let's get this adventure started. 🚀

Subscribe to the Developer DAO Newsletter. Probably Nothing

What is Irys?

Irys, formerly known as Bundlr, is a provenance layer on Arweave. It scales uploads to Arweave via bundling, lets you pay with different tokens than AR, and gives you strong provenance capabilities for your uploads.

Provenance means the source of the data. In the case of Arweave, who uploaded the data and when. The provenance of Irys is strong because it goes down to the millisecond and is immutable and permanent, so after your data is on Arweave, you can use the provenance tags of Irys to prove you’re its creator.

It is also cheaper and more convenient to use Irys for Arweave upload as you are not charged per chunk as when you upload to Arweave directly. If you only upload 1KB, you only pay for that, not the whole 256KB chunk used to store it.

Still, the fee is dynamic and dependent on several factors, like the exact number of bytes you are uploading and Arweave's cost, which is the cost of storing X amount of bytes on Arweave using linear pricing, etc. You can learn how the cost is computed in the Irys docs.

Transactions uploaded via Irys are deemed finalized once uploaded, as Irys guarantees that the data gets uploaded to Arweave. In contrast to Arweave, which has a block time of 2 minutes and takes 50 block confirmations for data to be finalized and stored permanently on Arweave, Irys does not have such restrictions, as it uses optimistic finality and retries the upload of data to Arweave until the data is included in a block and confirmed. You can safely assume that Irys finalizes any data uploaded via the network.

Prerequisite Knowledge

You should be familiar with working with JavaScript and know how to build React apps. You should know your way around the command line and be able to install NPM packages via the terminal. You should install Node.js v16 and NPM on your development machine and be familiar with an EVM-based wallet like MetaMask.

Creating the NextJS Application and Installing Dependencies

Navigate to your working directory, create a new directory, and initiate a new NPM project in the newly created directory, where we will install the NextJS application. Run the following command at the terminal:

npx create-next-app@latest

This command creates a new NextJS application in the directory. Please keep it simple and opt out of using the new NextJS app router.

Install the dependencies by running:

npm install @irys/sdk @irys/query axios formidable
  • @irys/sdk is used to upload data via the Irys to Arweave
  • @irys/query is the query package from Irys used to query for uploaded data and transactions

  • axios make network calls from the browser.

  • formidable to parse uploaded files on the server.

After installing these dependencies, run the application by typing on the command line:

npm run dev

The application should start and run on localhost:3000

Next, navigate to the root of your NextJS application and create an environment variable file called .env-local. This file will contain your wallet's private keys.

Add the .env-local file to your gitignore file, and never push it to a remote repository. It is advisable to use a disposable wallet key for this exercise; don't use the wallet key that contains your crypto assets!

Open the .env-local file and insert your wallet's private key.

Private_Key = insert-your-private-key-here

You are done creating a boilerplate NextJS application and can dive into the world of Irys to see how easy it is to use it to upload data to Arweave.

Initializing Irys

Create a new folder called utils in the app's root directory. Create a file inside the utils folder called utils.js. You will implement some relevant functions in this file.

At the top of the newly created utils.js file, import the Irys package

import Irys from "@irys/sdk";
`

Then copy and paste the following code inside the utils.js file:

const getIrysClient = () => {
    const irys = new Irys({
        url: "https://devnet.irys.xyz",
        token: "matic",
        key: Private_Key,
        config: {
            providerUrl: "https://rpc-mumbai.maticvigil.com",
        }
    });
    // Print your wallet address
    console.log(`wallet address = ${irys.address}`);
    return irys;
};

This code initializes Irys creating an Irys constructor that accepts an object with keys of url, token, key and config. The url is the Irys node we want to connect to, token is the currency to use for payment, key is the private key of the wallet and config is only necessary if we are connecting to a testnet which we are doing in this tutorial.

We can connect to three Irys networks of nodes, which are:

The first two listed networks are mainnet networks, which require real tokens for payments before you can use them to upload data. The last one is a testnet, where you can use testnet tokens.

The testnet deletes files after 90 days, while the mainnet network ensures your data is stored forever on Arweave.

The following figure shows the supported currencies:

Table showing supported payment currencies when using Irys on Arweave network

Since you are testing things out, you will use the devnet network with the Mumbai Polygon Matic token for payment. You can obtain a free testnet Matic token here.

Funding an Irys Node

You can fund a connected Irys Node using a pay-as-you-go model, which means you fund the node with the amount needed for the next upload.

You could also fund the node in advance, but you can only use the node you have funded, and you are also allowed to withdraw any unused funds from the node.
Open the file utils/utils.js and create a new function called lazyFundNode.

export const lazyFundNode = async (size) => {
    const irys = getIrysClient();
    const price = await irys.getPrice(size);
    await irys.fund(price);
};

The function is async and takes the size of the data you are uploading as a parameter. It calls the getIrysClient method, which we have previously defined, to obtain a new Irys client that uses Mumbai Matic to pay for uploads. Next, you await a call to the getPrice method to get the price for uploading an image/data of the passed parameter size. Finally, you await irys.fund(price), which causes the token to be withdrawn from your wallet to fund the node.

Upload File Function

Create and export a new function inside the utils/utils.js file called uploadFileToArweave. This is a simple function that does the file upload.
At the top of the utils.js, add an import statement for fs.

import * as fs from "fs";
export const uploadFileToArweave = async (filepath, tags) => {
    const irys = getIrysClient();
    const file = fs.readFileSync(filepath);
    const { id } = await irys.upload(file, { tags });
    console.log("file uploaded to ", `https://arweave.net/${id}`);
    return id;
};

The function uploadFileToArweave takes in two parameters: the filepath and tags. The function reads the file from the file system using fs.readFileSync(filepath). After reading the file into a buffer, it calls the irys.upload, passing the file buffer and Arweave tags.

Arweave tags, often simply called tags, are user-defined and are an array of objects with the following shape:

const tags = [ { name: "...", value: "..."} ]

You will learn how to use these tags to query uploaded data later.

Coding the Image Upload Page

Next, you'll code the page to allow users to select an image file for onward upload to the server for further processing.

Open the file pages/index.js, remove the previous content, and replace it with the following content:

import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin'] })
import React, { useState } from "react";

import ImageViewer from '@/components/ImageViewer';


const allowedFiles = (file) => {
  const fileTypes = ["image/png", "image/jpeg", "image/jpg", "image/gif"];
  return fileTypes.includes(file)
}


export default function Home() {
  const [imageSource, setImageSource] = useState(null);
  const [selectedFile, setSelectedFile] = useState(null)
  const [caption, setCaption] = useState("");
  const [description, setDescription] = useState("")
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const fileInputRef = React.useRef();

  const handleImageUpload = (event) => {

    if (event.target.files && event.target.files[0] && allowedFiles(event.target.files[0].type)) {
      setSelectedFile(event.target.files[0])
      const reader = new FileReader();
      reader.onload = function (e) {
        setImageSource(e.target.result);
      };
      reader.readAsDataURL(event.target.files[0]);
    } else {
      setImageSource(null);
    }
  };


  const uploadFileToArweave = async (event) => {
    event.preventDefault();
    try {
      if (selectedFile && caption && description) {
        setLoading(true);
        const formData = new FormData();
        //build the tags
        const applicationName = {
          name: "application-name",
          value: "image-album",
        };
        const applicationType = { name: "Content-Type", value: selectedFile.type }
        const imageCaption = { name: "caption", value: caption };
        const imageDescription = { name: "description", value: description }

        const metadata = [
          applicationName,
          imageCaption,
          imageDescription,
          applicationType
        ]

        formData.append("file", selectedFile);
 formData.append("metadata", JSON.stringify(metadata));
        const response = await fetch("/api/upload", {
          method: "POST",
          body: formData,
        });
       console.log ("response from the method: ",response.data)
      }
    } catch (error) {
      setError(error.message);
      console.log("error ", error);
    } finally {
      setLoading(false);
      setSelectedFile(null);
      setImageSource(null);
      setCaption("");
      setDescription("")
      fileInputRef.current.value = null;

    }
  };

  return (
    <React.Fragment>
    <div className="flex justify-center items-center">
      {error && <p>There was an error: {error}</p>}
      <div className="bg-white m-2 p-8 rounded shadow-md w-1/3">
        <h2 className="text-2xl mb-4">Upload Image</h2>
        <div className='flex-col'>
          <div>
            <label className="block text-sm font-medium text-gray-700 mb-2">Upload an Image</label>
            <input
              type="file"
              className="hidden"
              id="imageInput"
              onChange={handleImageUpload}
              ref={fileInputRef}
              accept=".png, .jpg, .jpeg"
            />
          </div>

          {/* Div to display selected image */}
          <div className="mt-2">
            {imageSource ? (
              <img className="mt-2 rounded-lg" src={imageSource} alt="Selected" />
            ) : (
              <p className="text-gray-400">No image selected</p>
            )}
          </div>

          <button
            className="mt-2 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
            onClick={() => document.getElementById('imageInput').click()}
          >
            Select Image
          </button>

        </div>
      </div>

      {selectedFile &&
        <div className='bg-white m-2 p-8 w-2/3'>
            <div className="bg-white p-8 m-4 rounded shadow-md">
              <h2 className="text-2xl mb-4">Image Details</h2>
              <div className="mb-4">
                <label className="block text-sm font-medium mb-1">Image Caption</label>
                <input
                  value={caption}
                  onChange={(e) => setCaption(e.target.value)}
                  type="text"
                  className="w-full border border-gray-300 px-3 py-2 rounded-md focus:ring focus:ring-blue-300"
                />
              </div>
              <div className="mb-4">
                <label className="block text-sm font-medium mb-1">Image Description</label>
                <textarea
                  value={description}
                  onChange={(e) => setDescription(e.target.value)}
                  className="w-full border border-gray-300 px-3 py-2 rounded-md resize-none focus:ring focus:ring-blue-300"
                  rows= "4"
                ></textarea>
              </div>
              <button
                disabled={loading}
                onClick={uploadFileToArweave}
                className="bg-green-500 text-white px-4 py-2 rounded-md hover:bg-green-600 focus:outline-none focus:ring focus:ring-green-300"
              >
                Upload Image
              </button>
            </div>

        </div>
      }

    </div>

    </React.Fragment>
  )
}

This page contains a file upload component to select images from the user's computer. Let's break down the important functions of this component.

The React states holding data, like the selected image, the image caption, and the description.

The function handleImageUpload is attached to the file uploader onChanged event.

The handleImageUpload function checks that the user has selected a file and that the file type is one of the allowed types.

The allowedFiles function checks if the mime type is "image/png", "image/jpeg", "image/jpg" or "image/gif". This check ensures simplicity, as the best way to check the content and type of a file is on the server side.

The browser's FileReader class reads the selected file and saves the result in the imageSource React state. The image file is saved in another state called selectedFile. The selected image is displayed with an image tag. After the image is selected, a form is indicated for the user to enter a caption and a description for the image. The caption and description are saved in a React state named caption and description.

The uploadFileToArweave function is attached to the UploadImage button's click event handler of the image details form. The function checks for the existence of a selected image, caption, and image description. You create a new FormData() to be passed to the server. The selected image and the metadata are appended to the FormData().

Sign up for Developer DAO's newsletter. Probably Nothing

You are creating three tags describing the image and one tag describing the application name. You can define an arbitrary number of tags; the only restriction is that the total size of the tags should not be more than 2KB. In defining tags we could include the creator of the particular data which then becomes associated with that piece of data.

 const applicationName = { name: "application-name",value: "image-album"};
 const applicationType = { name: "Content-Type", value: selectedFile.type }
 const imageCaption = { name: "caption", value: caption };
 const imageDescription = { name: "description", value: description }

The tags are pushed into an array that is appended to the form data with a key of metadata

 formData.append("file", selectedFile);
 formData.append("metadata", JSON.stringify(metadata));

Finally, you call the API endpoint on the server using axios, passing the formData in the request body.

const response = await fetch("/api/upload", {
          method: "POST",
          body: formData,
 });

Next, you will create the endpoint to receive and process the file for onward upload to Arweave.

Coding the Upload API Route

Create a new pages/api/upload.js file. Remember that the file name should match the endpoint. At the top of the upload.js file, import the following:

import formidable from "formidable";
import path from "path";
import * as fs from "fs";
import { lazyFundNode, uploadFileToArweave } from "../../utils/utils"

The formidable package is used to process the file coming from the client. You also imported the utility functions we had created earlier; lazyFundNode and uploadFileToArweave.

Next, you export a config object to configure NextJS to not automatically parse requests coming from the API route.

export const config = {
    api: {
        bodyParser: false,
    },
};

You manually parse the client request using formidable; remember, as the request will contain the uploaded file and the metadata fields, you will define a handler function to process the client request.

const handler = async (req, res) => {
    try {
        fs.mkdirSync(path.join(process.cwd() + "/uploads", "/images"), {
            recursive: true,
        });
        const { fields, files } = await readFile(req);
        const filepath = files.file[0].filepath;
        //get the size of the file we want to upload
        const { size } = fs.statSync(filepath);
        //fund the Node
        await lazyFundNode(size);
        //upload the file to Arweave
        const transId = await uploadFileToArweave(filepath,    JSON.parse(fields.metadata));
        fs.unlinkSync(filepath);
        res.status(200).json(transId);
    } catch (error) {
        console.log("error ", error)
        res.status(400).json({ error: error });
    }
};
export default handler;

The handler is asynchronous and receives a request and a response object as parameters. At the top of the file, we create a directory to store the uploaded image. The directory is created if it does not exist inside your application folder.

  fs.mkdirSync(path.join(process.cwd() + "/uploads", "/images"), {
            recursive: true,
  });

path.join adds the current working directory to the newly created directory "uploads/images" to get an absolute path. The function readFile processes the file and returns the file object and the fields sent from the client. Copy the code below and paste it into the upload.js file.

const readFile = (req) => {
    const options = {};
    options.uploadDir = path.join(process.cwd(), "/uploads/images");
    options.filename = (name, ext, path, form) => {
        return Date.now().toString() + "_" + path.originalFilename;
    };

    options.maxFileSize = 4000 * 1024 * 1024;
    const form = formidable(options);
    return new Promise((resolve, reject) => {
        form.parse(req, (err, fields, files) => {
            if (err) reject(err);
            resolve({ fields, files });
        });
    });
};

The readFile function returns a Promise, which resolves to the fields and files. You create an emptyoptionsobject to configureformidable. First, you set theuploadDir, which equates to theupload/imagesdirectory you previously created. You also set thefilename` and the maximum file size we want to upload to the server, in this case, 4MB.

You create a formidable instance by passing in these options.

const form = formidable(options);

The Promise resolves with the files and fields if successful or rejects with an error. After reading and processing the file, we get the file size using the fs.statsSync method, passing in the file path.

const { size } = fs.statSync(filepath);

You must fund the Irys node with the token amount required to upload the file. In this example, you are operating a pay-as-you-go method. You await the result of the function await lazyFundNode(size). Remember, this is one of the utility functions created earlier that accepts a size parameter to fund a node.

Next, you call the uploadFileToArweave function, passing in a file path and the metadata. This function was created earlier, and it processes and uploads the file to Arweave.

const transId = await uploadFileToArweave(filepath, 
                      JSON.parse(fields.metadata));

If all goes well, you get the transaction ID from Arweave. The uploaded file is then deleted from the server's file system.

fs.unlinkSync(filepath);

The API handler function returns the transaction ID to the client as a response.

Retrieving Uploaded Files From Arweave Network

So far, you have learned how to configure Irys and utilize it for uploading data to Arweave. Now, you will progress further and see how to retrieve data uploaded to Arweave.

We will use the Irys query package to retrieve data instead of using Graphql. Create a new file in the root of your project folder called queries.js. Inside the newly created queries.js file, we will create and export an instance of the query package; this exported instance will be used throughout our application.

Copy and paste the code inside the queries.js file.

import Query from "@irys/query";


export const myQuery = () => {
    const myQuery = new Query({ url: "https://devnet.irys.xyz/graphql" });
    return myQuery;
}

The Query object takes an object with a url key; this key is the node we want to query. myQuery is returned from the function above. This is what we are going to use to query for our uploaded data.

Displaying Uploaded Images

Create a new folder called components on the project's root directory. Create a new file inside the components folder called ImageViewer.js. Copy and paste the code below into the file.

import React, { useEffect, useState } from "react";
import { myQuery } from "@/queries";






const ImageViewer = () => {
    const [data, setData] = useState([]);
    const [loading, setLoading] = useState(false)


    const loadUploadedData = async () => {
        setLoading(true)
        const query = myQuery();
        const results = await query.search("irys:transactions").tags([{ name: "application-name", values: ["image-album"] }]).sort("ASC");
        console.log("the result of the transactions: ", results)
        setData(results);
        setLoading(false);
    }


    useEffect(() => {
        loadUploadedData()
    }, [])






    if (loading) {
        return <div>Loading...........</div>
    }


    return <div className="flex flex-wrap">
        {data &&
            data.map(({ tags, id, }) => (
                <div className="w-1/5 p-4" key={id}>
                    <img src={`https://arweave.net/${id}`} className="w-full h-auto rounded"
                        width={300}
                        height={300}
                        alt=""
                    />


                    {tags.map(({ name, value }) => {
                        if (name == "caption") {
                            return <h3 className="mt-2 text-lg font-semibold" key={value}>{value}</h3>
                        } else if (name == "description") {
                            return <p className="text-gray-500" key={value}>{value}</p>
                        }
                    })}
                </div>
            ))}
    </div>
}






export default ImageViewer

At the top of the file, you imported standard React stuff like useState and useEffect. Next, we imported the instance of the Irys Query package that was exported from the queries.js file.

import { myQuery } from "@/queries";


```javascript


    const loadUploadedData = async () => {
        setLoading(true)
        const query = myQuery();
        const results = await query.search("irys:transactions").tags([{ name: "application-name", values: ["image-album"] }]).sort("ASC");
        console.log("the result of the transactions: ", results)
        setData(results);
        setLoading(false);
    }



```
We defined an async function `loadUploadedData`. This function makes use of the query package to retrieve data. At the top of the file, we are changing the loading state to true, and we retrieve the instance of the query package that we had defined.

Then we a search on uploaded data transactions, narrowing it to transactions with a tag of `"application-name"` with a value of `"image-album"`. This gives us the image uploaded via our toy application sorted in ascending order.

````javascript
const results = await query.search("irys:transactions").tags([{ name: "application-name", values: ["image-album"] }]).sort("ASC");

The returned result is saved in the React state, and we set the loading state of the application to false.

The loadUploadedData function is called inside a useEffect hook with an empty dependency array, meaning we want to call the function only once.

The data returned from the node is destructured to get the uploaded image with the caption and description displayed on the page. Open the index.js page, and let's add the ImageViewer component to the page. Add the ImageViewer before the closing </ React.Fragment>.

Run the application and upload an image; when the image is uploaded, refresh the page to see the uploaded image.

Subscribe to Developer DAO's newsletter. Probably Nothing.

Final Thoughts

This blog post has demonstrated how to upload data to the Arweave network via Irys. You built a sample working application that could be customized to meet your needs.

I thank you for staying with the cause and getting to the end of this post. You can find code for the blog post on GitHub.