visit
Here is the deployed link if you want to try it out:
Well, Well, Let’s Go Through What Is Arweave and Bundlr First (From the Doc). If you enjoy videos like me, here is a video for you:
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.
Keep the completed code open on the side for a better understanding
Setup a Nextjs project with tailwind CSS and Chakra UIYou can use
yarn add @bundlr-network/client bignumber.js
As we are using Rainbow kit this step is very easy inindex.tsx
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>
)
}
}
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
const { chains, provider } = configureChains(
[chain.polygonMumbai],
[
jsonRpcProvider({ rpc: () => ({ http: process.env.NEXT_PUBLIC_ALCHEMY_RPC_URL }) }),
publicProvider(),
]
);
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
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(
"//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
const bundlr = new WebBundlr(
"//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
In order to use the devnet, you need to use
//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
NEXT_PUBLIC_ALCHEMY_RPC_URL=//polygon-mumbai.g.alchemy.com/v2/{{alchemy_project_id}}
Read, about all the supported currencies/ networks by bundlr
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
let response = await bundlrInstance.fund(amountParsed)
In index.tsx
, make these changes
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
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
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
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
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(`//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
Also published .