Skip to main content

Bundlr + NodeJS

Prefer Learning Via Video? Watch This.

Introduction

Bundlr is designed to work with both NodeJS on the server and with React, VanillaJS and other frameworks in the browser.

To use this tutorial, you will need a crypto wallet with a small amount of crypto ready to deploy. To setup an Arweave wallet and get a small bit of AR, check out this tutorial. You can also use free Matic Mumbai or Ethereum Goerli currencies with our devnet server.

This tutorial takes you through getting a wallet on the Arweave blockchain and building a server-side script that connects to a Bundlr node, funds it, and then uploads files to the permaweb. It should take you less than an hour to work though. Once you’re done you will fully grok our SDK.

We’ve created a GitHub repository with all the code and sample images for this project, feel free to download and follow along as you code.

Installing The SDK

These examples use NodeJS, an open-source server environment that allows you to use JavaScript to create applications. If you haven’t done so already, start by installing NodeJS. It’s totally free and can be found here. Many coding tutorials require NodeJS so there’s a good chance you have it already installed. If you’re not sure, run

node -v

from your command prompt. If you get a version number like "v16.14.0" back, you’re good to go. Otherwise, run the installer.

Next, create a new directory and cd into it.

mkdir bundlr-basics
cd bundlr-basics

And then install the SDK. using npm:

npm install @bundlr-network/client

or using yarn:

yarn add @bundlr-network/client

When you use npm or yarn to install the SDK, it will create a file titled package.json in your root directory. Open up that file and add "type" : "module" to the beginning, allowing you to use ES6 module imports.

A final version of the package.json is as follows

{
"type": "module",
"dependencies": {
"@bundlr-network/client": "^0.9.0"
}
}

Finally, run the following in your command prompt to create a new JavaScript file, then fire up your code editor of choice. All of the code you write will go into this file, and remember if you get lost you can see a completed version in GitHub.

touch bundlr-basics.js

Connecting To A Node

First up is importing your private key and using it to securely connect to a Bundlr node.

If you followed along with the previous tutorial and created an Arweave wallet, take the json file you downloaded and drag it to this project directory. If you’re using a wallet from another blockchain, you will need that wallet’s private key. Regardless of the wallet you use, be very careful with the private key. Make sure not to show it to anyone, and not to upload it to GitHub.

The following code imports the Bundlr SDK, loads your private key, connects to a Bundlr node, and then prints your wallet’s address.

import Bundlr from "@bundlr-network/client";
import fs from "fs";

// Change this line to match the name of the private key file
// you downloaded from https://faucet.arweave.net/.
// Physically move your key file from the download directory to the
// project directory that holds this JS file.
const privateKey = "";

const jwk = JSON.parse(fs.readFileSync(privateKey).toString());
const bundlr = new Bundlr("http://node1.bundlr.network", "arweave", jwk);

// Print your wallet address
console.log(`wallet address = ${bundlr.address}`);

Paste the above code into the bundlr-basics.js file you created and then run it by typing node bundlr-basics.js in your command line. If it successfully prints your wallet address, you’re good to go.

Get Funded Balance

When you fund a node, you fund that one specific node. Your balance exists there and there only, not on other nodes. At present there are two nodes (http://node1.bundlr.network and http://node2.bundlr.network), you are welcome to use either one.

Regardless of which node you use, your data will still be stored on the same Arweave blockchain.

The function bundlr.getLoadedBalance() returns your wallet’s balance on that node in atomic units. You can convert that into an easy to read format using bundlr.utils.fromAtomic(atomicBalance).

note

Atomic units are a way to represent a floating point (decimal) number using non-decimal notation. They are commonly used to help ensure accuracy when performing mathematical operations on fractional numbers in JavaScript,

// Get loaded balance in atomic units
const atomicBalance = await bundlr.getLoadedBalance();
console.log(`node balance (atomic units) = ${atomicBalance}`);

// Convert balance to an easier to read format
const convertedBalance = bundlr.utils.fromAtomic(atomicBalance);
console.log(`node balance (converted) = ${convertedBalance}`);

Again, return to your command line and run the code again by typing node bundlr-basics.js. As you have yet to fund a node, the results should show a balance of 0.

Getting Upload Price

The 0.02 AR you received for free in the previous tutorial will go pretty far. This next code block shows how to check the price to upload 1 megabyte of data. The function bundlr.getPrice(numBytes) returns the price in atomic units to upload the specified number of bytes. Since 1 megabyte is 1048576 bytes, we check that number. As with before, we convert the result from atomic units to make it easier to understand.

At the time these docs were written, it costs $0.000339323387 USD to upload 1 Megabyte of data.

// Check the price to upload 1MG of data
// The function accepts a number of bytes, so to check the price of
// 1MG, you'll need to check the price of 1,048,576 bytes.
const dataSizeToCheck = 1048576;
const price1MGAtomic = await bundlr.getPrice(dataSizeToCheck);

// To ensure accuracy when performing mathematical operations
// on fractional numbers in JavaScript, it is common to use atomic units.
// This is a way to represent a floating point (decimal) number using non-decimal notation.
// Once we have the value in atomic units, we can convert it into something easier to read.
const price1MGConverted = bundlr.utils.fromAtomic(price1MGAtomic);
console.log(`Uploading 1MG to Bundlr costs $${price1MGConverted}`);

A common use-case is to check the cost to upload a file based on its size.

import Bundlr from "@bundlr-network/client";
import { statSync } from "fs";

const bundlr = new Bundlr("https://node1.bundlr.network", "solana", myPrivateKey);
await bundlr.ready();

const pathToFile = "./llama.png";
const { size } = statSync(pathToFile);
const price = await bundlr.getPrice(size);

Funding A Node

This next code block funds the node with enough to cover a full megabyte of data. In your projects, you’re welcome to fund once for all files that will be uploaded or do lazy funding where you only fund enough for each file as needed.

The choice as to which funding technique to use depends both on your project and the currency you’re using. When funding with AR, it can take upwards of 40 minutes before the balance shows up. MATIC and SOL generally post much faster, usually <10s.

Assuming you’re following along using the free AR you got for setting up your wallet, you may need to take a break after funding. Go grab some tea, take a walk, and stretch your legs, when you come back your balance will have posted.

You’ll know the balance has been posted when the code you wrote earlier in "Get balance" returns a number greater than 0.

// Fund the node, give it enough so you can upload a full MG
try {
// response = {
// id, // the txID of the fund transfer
// quantity, // how much is being transferred
// reward, // the amount taken by the network as a fee
// target, // the address the funds were sent to
// };
const response = await bundlr.fund(price1MGAtomic);
console.log(`Funding successful txID=${response.id} amount funded=${response.quantity}`);
} catch (e) {
console.log("Error funding node ", e);
}

You can also lazy-fund a node where you check the cost to upload each file first and then only transfer the exact cost for that file. This works best with currencies like MATIC, ETH and SOL whose balances post (almost) instantly.

import Bundlr from "@bundlr-network/client";
import * as fs from "fs";

const bundlr = new Bundlr("https://node1.bundlr.network", "matic", myPrivateKey);

const pathToFile = "./llama.png";
const { size } = await fs.promises.stat(pathToFile);
const price = await bundlr.getPrice(size);
await bundlr.fund(price);

const { id } = await bundlr.uploadFile(pathToFile);
console.log(`${pathToFile} --> Uploaded to https://arweave.net/${id}`);

Withdrawing Funds

Funds deposited into a node can be easily withdrawn at any time. The function bundlr.withdrawBalance(amountInAtomicUnits) accepts an amount (in atomic units) to withdraw and returns a response object that includes transaction id, requested amount, network fees and final cost.

In the code below, we check our funded balance and then withdraw it all.

try {
// 400 - something went wrong
// response.data = "Not enough balance for requested withdrawal"

// 200 - Ok
// response.data = {
// requested, // the requested amount,
// fee, // the reward required by the network (network fee)
// final, // total cost to your account (requested + fee)
// tx_id, // the ID of the withdrawal transaction
// }
// 1. Get current balance
const curBalance = await bundlr.getLoadedBalance();
// 2. Withdraw all
const response = await bundlr.withdrawBalance(curBalance);

console.log(`Funds withdrawn txID=${response.data.tx_id} amount requested=${response.data.requested}`);
} catch (e) {
console.log("Error funding node ", e);
}

Uploading Data

If it can be reduced to 1s and 0s, you can upload it via Bundlr to Arweave. The function bundlr.upload(data) uploads any data passed to it. Use this for projects like blockchain games where you might need to store data about a player’s character stats on the blockchain.

// Upload data
// If it can be reduced to 1s and 0s, you can store it via Bundlr.
const dataToUpload = "Hello world ... where the llamas at?";
try {
const response = await bundlr.upload(dataToUpload); // Returns an axios response
console.log(`Data uploaded ==> https://arweave.net/${response.id}`);
} catch (e) {
console.log("Error uploading file ", e);
}

Uploading A File

If you’re following along with the GitHub repository, you may have noticed a file called large_llama.png. This next code block shows how to upload that file to the Arweve permaweb using Bundlr. You are welcome to swap out this file for anything else you may want to upload, but remember the permaweb is permanent. Once uploaded, you can’t delete it. Be very careful to not upload anything private or personal.

Upload a file using the function bundlr.uploadFile(pathToFile+fileName), this returns a Promise that once resolved contains the transaction ID used when uploading. That transaction ID then combines with https://arweave.net/ to create a perma-URL pointing to your file.

You can check out my llama here https://arweave.net/CO9EpX0lekJEfXUOeXncUmMuG8eEp5WJHXl9U9yZUYA

// Upload a file
// Practice uploading with this lovely llama, or use any file you own.
// You've got 1MG of data paid for, so choose whatever you want.
// BUT ... REMEMBER ... You CAN'T DELETE THE FILE ONCE UPLOADED, SO BE CAREFUL! :)
const fileToUpload = "large_llama.png";
try {
const response = await bundlr.uploadFile("./" + fileToUpload); // Returns an axios response
console.log(`File uploaded ==> https://arweave.net/${response.id}`);
} catch (e) {
console.log("Error uploading file ", e);
}

Uploading A Folder

In addition to uploading arbitrary data and single files, you can batch upload an entire folder of files all at once. This is perfect for NFT projects that might want to upload thousands of files in one go. The function bundlr.uploadFolder(pathToFolder+folderName) accepts a path to a folder and then uploads all files in that folder.

While previous functions returned an URL pointing directly to a file, bundlr.uploadFolder(pathToFolder+folderName) returns a manifest ID you can use to determine the URL of the files.

Again using the sample files in GitHub, the code below uploads five additional llama pictures all contained in the folder titled "llama_folder". The images are named sequentially 1.png to 5.png.

note

It is NOT necessary to follow any specific naming convention for your files, feel free to name them as you wish.

// Upload an entire folder
// More llamas for you to upload ... or change to your own files
// Upload some NFTs, your vacation photos or your band's latest album.
const folderToUpload = "llama_folder";
try {
const response = await bundlr.uploadFolder("./" + folderToUpload, {
indexFile: "", // optional index file (file the user will load when accessing the manifest)
batchSize: 50, //number of items to upload at once
keepDeleted: false, // whether to keep now deleted items from previous uploads
}); //returns the manifest ID

console.log(`Files uploaded ==> Manifest Id = ${response.id}`);
} catch (e) {
console.log("Error uploading file ", e);
}

After running code, it returns output similar to (your manifest ID will differ):

Files uploaded ==> Manifest Id = B5lWpjakdcVtyHNHhXwvc1RBEgENfD_8YA7ANKofwSw

To generate links to each of your images you have two options.

  1. Append the original file name to the end of the link. Meaning the link to the first image (1.png) would become: https://arweave.net/B5lWpjakdcVtyHNHhXwvc1RBEgENfD_8YA7ANKofwSw/1.png. (https://arweave.net/manifest-id/original-file-name)
  2. Access the files using their direct URLs. After a successful folder upload, two files are written to your local project directory llama_folder-manifest.csv and llama_folder-manifest.json. Both files contain the same data, here’s my llama_folder-manifest.json.

llama-folder

Both of these files contain a list of the files uploaded and their unique transaction ID. Again returning back to my first file 1.png, it can be accessed directly via https://arweave.net/x9v9NpQCAacTadzLJJ6H1vNttoy03rcBgJwbLGitNzQ

The bundlr.uploadFolder() function takes one required parameter pointing to the folder to be uploaded along with three optional parameters.

  1. indexFile: The name of an index file also included in the folder you upload. If provided, this index file will load when a user requests https://arweave.net/manifest-id
  2. batchSize: The number of files to upload at once.
  3. keepDeleted: Whether to keep now deleted items from previous uploads. When you upload a folder, the files names are indexed and saved. If you then re-upload that same folder, Bundlr will only upload new files added since the previous upload. This helps prevent uploading the same files over and over. In the case where you delete files from your source directory, this flag tells Bundlr whether to keep those deleted files in the index or remove them.

Conclusion

Cool, cool, cool. Great job making it this far. If you want to challenge yourself and take this code a bit further, there are lots of small projects you could build on top. Things like:

  1. A command-line journal: Write from terminal and save your thoughts to the permaweb.
  2. Vacation photo archive: Pick your best trip photos, create an index page, upload the entire folder and share with your friends and family.
  3. Kids’ sports memories: Move your favorite sports moments from your crowded phone to a home on the permaweb that your kids can view and enjoy forever. Upload from the command line and then build a simple UI to track them.

And most importantly, make sure to share what you build.

We can’t wait to see it! LFB (Let’s F***ing Bundle!)