August 11, 2023

A decentralized video sharing app with Arweave, Bundlr, GraphQL, and Next.js

Full Stack Arweave Workshop

Building a decentralized video sharing app with Arweave, Bundlr, GraphQL, and Next.js.

BUILDING FULLY DECENTRALIZED FULL STACK APPLICATIONS WITH ARWEAVE AND NEXT.JS

Prerequisites

  1. Node.js installed on your machine

I recommend using either NVM or FNM for Node.js installation

  1. Matic, Arbitrum, or Avalanche tokens

  2. Metamask installed as a browser extension

  3. Fund your Bundlr wallet here with around $1.00 of your preferred currency.

Getting started

To get started, create a new Next.js application

npx create-next-app arweave-app

Next, change into the directory and install the dependencies using either NPM, Yarn, PNPM, or your favoriate package manager:

cd arweave-app

npm install @bundlr-network/client arweave @emotion/css ethers react-select

Overview of some of the dependencies

@emotion/css - CSS in JavaScript library for styling

react-select - select input control library for React

@bundlr-network/client - JavaScript client for interacting with Bundlr network

arweave - The Arweave JavaScript library

Base setup

Now that the dependencies are installed, create a new file named context.js in the root directory. We will use this file to initialize some React context that we'll be using to provide global state between routes.

// context.js
import { createContext } from 'react'

export const MainContext = createContext()

Next, let's create a new page in the pages directory called _app.js.

Here, we want to get started by enabling the user to sign in to bundlr using their MetaMask wallet.

We'll pass this functionality and some state into other pages so that we can use it there.

Add the following code to pages/app.js:

// pages/_app.js
import '../styles/globals.css'
import { WebBundlr } from "@bundlr-network/client"
import { MainContext } from '../context'
import { useState, useRef } from 'react'
import { providers, utils } from 'ethers'
import { css } from '@emotion/css'
import Link from 'next/link'

function MyApp({ Component, pageProps }) {
  const [bundlrInstance, setBundlrInstance] = useState()
  const [balance, setBalance] = useState(0)

  // set the base currency as matic (this can be changed later in the app)
  const [currency, setCurrency] = useState('matic')
  const bundlrRef = useRef()

  // create a function to connect to bundlr network
  async function initialiseBundlr() {
    await window.ethereum.enable()

    const provider = new providers.Web3Provider(window.ethereum);
    await provider._ready()

    const bundlr = new WebBundlr("https://node1.bundlr.network", currency, provider)
    await bundlr.ready()

    setBundlrInstance(bundlr)
    bundlrRef.current = bundlr
    fetchBalance()
  }

  // get the user's bundlr balance
  async function fetchBalance() {
    const bal = await bundlrRef.current.getLoadedBalance()
    console.log('bal: ', utils.formatEther(bal.toString()))
    setBalance(utils.formatEther(bal.toString()))
  }

  return (
    
  )
}

const navHeight = 80
const footerHeight = 70

const navStyle = css`
  height: ${navHeight}px;
  padding: 40px 100px;
  border-bottom: 1px solid #ededed;
  display: flex;
  align-items: center;
`

const homeLinkStyle = css`
  display: flex;
  flex-direction: row;
  align-items: center;
`

const homeLinkTextStyle = css`
  font-weight: 200;
  font-size: 28;
  letter-spacing: 7px;
`

const footerStyle = css`
  border-top: 1px solid #ededed;
  height: ${footerHeight}px;
  padding: 0px 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 200;
  letter-spacing: 1px;
  font-size: 14px;
`

const containerStyle = css`
  min-height: calc(100vh - ${navHeight + footerHeight}px);
  width: 900px;
  margin: 0 auto;
  padding: 40px;
`

export default MyApp

What have we done here?

  1. Imported the dependencies
  2. Created some component state, one to hold the instance of Bundlr, the other to hold the user's wallet balance.
  3. Created a function to connect to bundlr - initialiseBundlr
  4. Created a function to fetch the user's balance - fetchBalance
  5. Added some basic styling using emotion
  6. Added some navigation, a footer, and a link in the footer to the profile page that has not yet been created.

Next, let's run the app:

npm run dev

You should see the app load and have a header and a footer! 🎉🎉🎉

Connecting to Bundlr

Next, let's create the UI that will allow the user to choose the currency they'd like to use and connect to Bundlr.

To do so, create a new file in the pages directory named profile.js. Here, add the following code:

import { useState, useContext } from 'react'
import { MainContext } from '../context'
import { css } from '@emotion/css'
import Select from 'react-select'

// list of supported currencies: https://docs.bundlr.network/docs/currencies
const supportedCurrencies = {
  matic: 'matic',
  ethereum: 'ethereum',
  avalanche: 'avalanche',
  bnb: 'bnb',
  arbitrum: 'arbitrum'
}

const currencyOptions = Object.keys(supportedCurrencies).map(v => {
  return {
    value: v, label: v
  }
})

export default function Profile() {
  // use context to get data and functions passed from _app.js
  const { balance, bundlrInstance, initialiseBundlr, currency, setCurrency } = useContext(MainContext)

  // if the user has not initialized bundlr, allow them to
  if (!bundlrInstance) {
    return  (
      
setCurrency(value)} options defaultValue={{ value: currency, label: currency }} classNamePrefix="select" instanceId="currency" />

Currency:/p>

) } {/* most of this UI is also new */} return (

💰 Balance {Math.round(balance * 100) / 100}

{ /* if there is a video save to local state, display it */} { localVideo && ( ) } {/* display calculated upload cast */} { fileCost &&

Cost to upload: {Math.round((fileCost) * 1000) / 1000} MATIC

}