Permanent File Storage for Web3 Apps with Arweave, Bundlr, Next.js, RainbowKit, and Wagmi
Tags: web3, blockchain, arweave, bundlr
Published: 2022-06-27 16:02:48

In this article I will be showing you how to create a Dapp that allows us to store data on Arweave network, using bundlr on testnet.

Before going forward let's see what are we going to build today 

{% embed https://www.loom.com/share/808aff5aa5d84ead93c106cfb96ff05f %} 
<br />
**Here is the deployed link if you want to try it out:** https://bundlr-arweave.netlify.app/ 
<br />

### Tech stack and libraries I've used:
- Nextjs
- Chakra UI
- RainbowKit
- Wagmi 
- Bundlr Client

<br />

## Well well let's go through what is Arweave and Bundlr first (from the doc)

If you are a video person like me here is a video for you 

{% embed https://www.youtube.com/watch?v=DQbwiiXMtMo %}

## [Arweave:](https://www.arweave.org/)
Arweave makes information permanence sustainable.
Arweave is a new type of storage that backs data with sustainable and perpetual endowments, allowing users and developers to truly store data forever – for the very first time.

## [Bundlr:](https://docs.bundlr.network/docs/about/introduction)
Bundlr makes web3 data storage on Arweave accessible by making it as fast, easy, and reliable as traditional data storage. Arweave is the only truly decentralized, permanent data storage solution. Bundlr increases the amount of transactions conducted on Arweave by 4000% without sacrificing security or usability, and is around ~3000x faster at uploading data.

Bundlr currently accounts for over 90% of data uploaded to Arweave. It enables infinite scalability and enables instant and guaranteed transaction finality. It is a multi-chain solution and is compatible with leading blockchains including Ethereum, Solana, Avalanche, Polygon, and many more.

<br />
## Steps we have to follow to upload to Arweave using Bundlr

1. Connect wallet
2. Initialize the Bundlr client 
3. Add funds ($BNDLR tokens)
4. Upload the files 

> **Note:** we will be bulding this app on polygon mumbai testnet
> 

> Bundlr has a devnet which allows you to use devnet/testnet cryptocurrency networks to pay for storage. The devnet node behaves exactly as a mainnet node - other than data is never moved to Arweave and will be cleared from Bundlr after a week.
>

<br />

## Let's get building

Keep the completed code open on the side for a better understanding **[Github](https://github.com/Deep1144/bundlr-arweave-testnet-uploader)**

### 1. Setting up the project

Setup a Nextjs project with tailwind CSS and Chakra UIYou can use [create-web3-frontend](https://github.com/dhaiwat10/create-web3-frontend) package to get done with setup quickly or just clone this [project repo](https://github.com/Deep1144/bundlr-arweave-testnet-uploader)

Also, install these two dependencies 

```bash
yarn add @bundlr-network/client bignumber.js
```

### 2. Connect wallet

As we are using Rainbow kit this step is very easy 
in `index.tsx`

```bash
const Home: NextPage = () => {
  const { data } = useAccount();
  if (!data) {
    return (
      <div className='justify-center items-center h-screen flex '>
        <VStack gap={8}>
          <Text className='text-4xl font-bold'>
            Connect your wallet first
          </Text>
          <ConnectButton />
        </VStack>
      </div>
    )
  }

  if (activeChain && activeChain.id !== chainId.polygonMumbai) {
    return (
      <div className='justify-center items-center h-screen flex '>
        <VStack gap={8}>
          <Text className='text-4xl font-bold'>
            Opps, wrong network!! Switch to Polygon Mumbai Testnet
          </Text>
          <ConnectButton />
        </VStack>
      </div>
    )
  }
}
```

And with these 5–6 lines of code, we are done with connecting the wallet 🎉

We have also added a condition to check if a user is on the Mumbai test, as we will be using polygon Mumbai testnet for this article

In `_app.jsx` while configuring Rainbow kit we have configures it for using polygon Mumbai testnet

```jsx
const { chains, provider } = configureChains(
  [chain.polygonMumbai],
  [
    jsonRpcProvider({ rpc: () => ({ http: process.env.NEXT_PUBLIC_ALCHEMY_RPC_URL }) }),
    publicProvider(),
  ]
);
```

So, Rainbow kit will also take care of changing the network for us, this is how this warning will look like

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n9oxmjnls710mzjxpj4o.png)
 

### 3. Initializing the Bundlr

let’s create a context for bundlr, so we can maintain all bundlr related logic form there 

Create `bundlr.context.tsx`

Inside this file let's create the context now, you can see the completed version of the file [here](https://github.com/Deep1144/bundlr-arweave-testnet-uploader/blob/main/state/bundlr.context.tsx) 

```jsx
import { WebBundlr } from '@bundlr-network/client';
import { useToast } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { providers, utils } from 'ethers';
import React, { createContext, useContext, useEffect, useState } from 'react'

const BundlrContext = createContext<IBundlrHook>({
    initialiseBundlr: async () => { },
    fundWallet: (_: number) => { },
    balance: '',
    uploadFile: async (_file) => { },
    bundlrInstance: null
});

const BundlrContextProvider = ({ children }: any): JSX.Element => {
    const toast = useToast()
    const [bundlrInstance, setBundlrInstance] = useState<WebBundlr>();
    const [balance, setBalance] = useState<string>('');

    useEffect(() => {
        if (bundlrInstance) {
            fetchBalance();
        }
    }, [bundlrInstance])

    const initialiseBundlr = async () => {
        const provider = new providers.Web3Provider(window.ethereum as any);
        await provider._ready();
        const bundlr = new WebBundlr(
            "https://devnet.bundlr.network",
            "matic",
            provider,
            {
                providerUrl:
                    process.env.NEXT_PUBLIC_ALCHEMY_RPC_URL,
            }
        );
        await bundlr.ready();
        setBundlrInstance(bundlr);
    }

    async function fundWallet(amount: number) {
        try {
            if (bundlrInstance) {
                if (!amount) return
                const amountParsed = parseInput(amount)
                if (amountParsed) {
                    toast({
                        title: "Adding funds please wait",
                        status: "loading"
                    })
                    let response = await bundlrInstance.fund(amountParsed)
                    console.log('Wallet funded: ', response)
                    toast({
                        title: "Funds added",
                        status: "success"
                    })
                }
                fetchBalance()
            }
        } catch (error) {
            console.log("error", error);
            toast({
                title: error.message || "Something went wrong!",
                status: "error"
            })
        }
    }

    function parseInput(input: number) {
        const conv = new BigNumber(input).multipliedBy(bundlrInstance!.currencyConfig.base[1])
        if (conv.isLessThan(1)) {
            console.log('error: value too small')
            toast({
                title: "Error: value too small",
                status: "error"
            })
            return
        } else {
            return conv
        }
    }

    async function fetchBalance() {
        if (bundlrInstance) {
            const bal = await bundlrInstance.getLoadedBalance();
            console.log("bal: ", utils.formatEther(bal.toString()));
            setBalance(utils.formatEther(bal.toString()));
        }
    }

    async function uploadFile(file) {
        try {
            let tx = await bundlrInstance.uploader.upload(file, [{ name: "Content-Type", value: "image/png" }])
            return tx;
        } catch (error) {
            toast({
                title: error.message || "Something went wrong!",
                status: "error"
            })
        }
    }

    return (
        <BundlrContext.Provider value={{ initialiseBundlr, fundWallet, balance, uploadFile, bundlrInstance }}>
            {children}
        </BundlrContext.Provider>
    )
}

export default BundlrContextProvider;

export const useBundler = () => {
    return useContext(BundlrContext);
}
```

For initializing the Bundlr we will be using `initialiseBundlr` the method from the code above 

and the code is also pretty straightforward

```jsx
const bundlr = new WebBundlr(
            "https://devnet.bundlr.network",
            "matic",
            provider,
            {
                providerUrl:
                    process.env.NEXT_PUBLIC_ALCHEMY_RPC_URL,
            }
        );
        await bundlr.ready();
        setBundlrInstance(bundlr);
```

We are creating an instance of the Bundlr client and storing it in a state variable

Read more about [bundlr client](https://docs.bundlr.network/docs/client/js)

For using Bundlr with testnet [read here](https://docs.bundlr.network/docs/devnet)

> In order to use the devnet, you need to use `https://devnet.bundlr.network` as the node and set the provider url to a correct testnet/devnet RPC endpoint for the given chain.
> 

our, RPC URL will look something like this, as we will be using polygon testnet 

```jsx
NEXT_PUBLIC_ALCHEMY_RPC_URL=https://polygon-mumbai.g.alchemy.com/v2/{{alchemy_project_id}}
```

Read, about all the supported **currencies/ networks by bundlr** [here](https://docs.bundlr.network/docs/currencies).
****

### 4. Add funds ($BNDLR tokens) 💸

We already have data about our `balance` in the bundlr context, and we also have a method `fundWallet` which we will be using to add the funds 

Adding funds is also pretty straightforward, just use the `fund` method on the bundlrInstance and pass the amount you want to fund the wallet with

```jsx
let response = await bundlrInstance.fund(amountParsed)
```

in `index.tsx`, make these changes

```jsx
const Home: NextPage = () => {
	const { initialiseBundlr, bundlrInstance, balance } = useBundler();
	...

	// at last
	if (!balance || Number(balance) <= 0) {
	    return (
	      <div className='justify-center items-center h-screen flex '>
	        <VStack gap={8}>
	          <ConnectButton />
	          <Text className='text-4xl font-bold'>
	            Opps, out of funds!, let's add some
	          </Text>
	          <FundWallet />
	        </VStack>
	      </div>
	    )
	  }
}
```

Now let’s create the `<FundWallet />` component 

Create `components/FundWallet.tsx`

```jsx
import React from 'react'
import { Button, NumberDecrementStepper, NumberIncrementStepper, NumberInput, NumberInputField, NumberInputStepper, Text, VStack } from '@chakra-ui/react'
import { useBundler } from '@/state/bundlr.context';

const FundWallet = () => {
    const { fundWallet, balance } = useBundler();
    const [value, setValue] = React.useState('0.02')

    return (
        <div className='mt-12'>
            <VStack gap={6}>
                <Text fontSize={'xl'}>
                    Your current balace is: {balance || 0} $BNDLR
                </Text>
                <NumberInput className='mx-auto' step={0.01} defaultValue={value}
                    onChange={(valueString) => setValue(valueString)}
                >
                    <NumberInputField />
                    <NumberInputStepper >
                        <NumberIncrementStepper />
                        <NumberDecrementStepper />
                    </NumberInputStepper>
                </NumberInput>

                <Button onClick={() => fundWallet(+value)}>💸 Add Fund</Button>
            </VStack>
        </div>
    )
}

export default FundWallet
```

### 5. Upload images  📷

Now, as we’ve already added the funds, now we can upload the images (or any other files), to the Arweave network 

In `index.tsx` , add the following code at the end of the file

```jsx
return (
    <div className='justify-center items-center h-screen flex'>
      <Stack direction={['column', 'row']} justifyContent={'space-around'} width={'full'} alignItems={'center'}>
        <VStack gap={8}>
          <ConnectButton />
          <FundWallet />
        </VStack>
        <VStack gap={8}>
          <Text fontSize={'4xl'}>
            Select Image To Upload
          </Text>
          <UploadImage />
        </VStack>
      </Stack>
    </div>
  );
```

Now, let’s create the `<UploadImage />` component 

```tsx
import React from 'react'
import { Box, Button, Text } from '@chakra-ui/react';
import { useBundler } from '@/state/bundlr.context';
import { useRef, useState } from 'react';

const UploadImage = () => {
    const { uploadFile } = useBundler();
    const [URI, setURI] = useState('')
    const [file, setFile] = useState<Buffer>()
    const [image, setImage] = useState('')
    const hiddenFileInput = useRef(null);

    function onFileChange(e: any) {
        const file = e.target.files[0]
        if (file) {
            const image = URL.createObjectURL(file)
            setImage(image)
            let reader = new FileReader()
            reader.onload = function () {
                if (reader.result) {
                    setFile(Buffer.from(reader.result as any))
                }
            }
            reader.readAsArrayBuffer(file)
        }
    }

    const handleClick = event => {
        hiddenFileInput.current.click();
    };

    const handleUpload = async () => {
        const res = await uploadFile(file);
        setURI(`http://arweave.net/${res.data.id}`)
    }

    return (
        <div className='flex flex-col mt-20 justify-center items-center w-full'>
            <Button onClick={handleClick} className='mb-4'>
                {image ? 'Change Selection' : 'Select Image'}
            </Button>
            <input
                accept="image/png, image/gif, image/jpeg"
                type="file"
                ref={hiddenFileInput}
                onChange={onFileChange}
                style={{ display: 'none' }}
            />

            {
                image &&
                <Box
                    display='flex'
                    alignItems='center'
                    justifyContent='center'
                    width='100%'
                    py={40}
                    bgImage={`url('${image}')`}
                    bgPosition='center'
                    bgRepeat='no-repeat'
                    mb={2}
                >
                    <button className='bg-gray-200 rounded px-8 py-2 text-black hover:bg-gray-100' onClick={handleUpload}>Upload File</button>
                </Box>
            }

            {
                URI && <Text className='mt-4'>
                    <Text fontSize='xl'> Uploaded File:</Text> <a href={URI} target="_blank">{URI}</a>
                </Text>
            }
        </div>
    )
}

export default UploadImage
```

And, with this in the place, we can now upload the files to the Arweave network 🚀

### Further Improvement:

1. Right now we are only uploading images, we can add support for other formats as well (video, audio, anything)
2. Store URLs of uploaded images with wallet address in Supabase and show the list of files previously uploaded by users when they connect their wallet

Completed code: [github](https://github.com/Deep1144/bundlr-arweave-testnet-uploader)

Want to Connect?
Connect with me on Twitter: [@pateldeep_eth ](https://twitter.com/pateldeep_eth)
Linkedin: [Linkedin](https://www.linkedin.com/in/patel-deep-dev/)
Originally published at https://pateldeep.xyz/