Uploading Files to Arweave with Irys
Table of contents
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. 🚀
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 transactionsaxios
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:
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()
.
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 empty
optionsobject to configure
formidable. First, you set the
uploadDir, which equates to the
upload/imagesdirectory you previously created. You also set the
filename` 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.
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.
Subscribe to my newsletter
Read articles from Osikhena Oshomah directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Osikhena Oshomah
Osikhena Oshomah
Javascript developer working hard to master the craft.