Building Full Stack Permanent Applications with Arweave, Smartweave, and Next.js
Tags: webdev, javascript, nextjs, web3
Published: 2022-09-10 19:12:27

---
title: Building Full Stack Permanent Applications with Arweave, Smartweave, and Next.js
published: true
description: How to build full stack dapps on the permaweb.
tags: webdev, javascript, nextjs, web3
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r2o66tc2bwbgr07i97o4.png
---

> The code for this tutorial can be found [here](https://github.com/dabit3/full-stack-warp-arweave)

In this tutorial you'll learn how to build full stack dapps on Arweave with [Smarweave](https://arweave.medium.com/introducing-smartweave-building-smart-contracts-with-arweave-1fc85cb3b632), [Warp](https://warp.cc/), and [Next.js](https://nextjs.org/).

Smartweave TLDR
- Code smart contracts in JS, TS, or Rust
- Execute arbitrary amounts of compute without additional fees
- Never have to worry about gas optimizations
- No state bloat
- Can directly process rich content / large files
- [Warp](https://warp.cc/) offers enhancements (speed, caching, sdks)

The application we'll be building is a full stack blog, meaning that you will have an open, public, and composable back end that can be transferred and reused anywhere (not only in this app).

Unlike most blockchain applications working with large or arbitrary amounts of data, Smartweave enables all of the state for this app to be stored directly on-chain.

I think this is a good example as it's not too basic as to be boring, but not too complex as to be confusing. It shows how to do most of the basic things you'll need and want to understand in order to build more complex and sophisticated applications going forward.

### About Arweave

[Arweave](https://arwiki.wiki/#/en/main) is a web3 protocol that allows developers to permanently store files like images, videos, and pdfs as well as single page web applications.

Arweave introduced the idea of the __permaweb__ - a permanent, global, community-owned web that anyone can contribute to or get paid to maintain.

## Smartweave

Arweave also introduced [SmartWeave](https://arweave.medium.com/introducing-smartweave-building-smart-contracts-with-arweave-1fc85cb3b632): a smart contract protocol that allows developers to build permanent applications on top of Arweave.

When you publish a Smartweave contract, the program’s source code and its initial state is stored in an Arweave transaction.

When a user writes an update to a SmartWeave program, they write their inputs as a new Arweave transaction.

To calculate the state of the contract, a SmartWeave client uses the contract source code to execute the history of inputs sequentially. Invalid transactions are ignored.

In doing so, SmartWeave pushes the responsibility of validating transactions onto the users.

### Warp

Warp (https://warp.cc/) is a protocol built on top of Arweave meant to facilitate a better DX/UX for Smartweave application development.

Warp consists of 3 main layers:

1. The Core Protocol layer is the implementation of the original SmartWeave protocol and is responsible for communication with the SmartWeave smart contracts deployed on Arweave

2. The Caching layer - is build on top of the Core Protocol layer and allows caching results of each of the Core Protocol modules separately.

    This allows you to quickly retrieve data from contracts with a large number of state updates, but also offers instant transactions and contracts availability and finality.

3. Extensions layer - a CLI, Debugging tools, different logging implementations, so called "dry-runs" (i.e. actions that allow to quickly verify the result of given contract interaction without writing anything on Arweave).

## Getting started

Now that we know a little bit about the underlying technology, let's start building.

### Prerequisites

To be successful in this tutorial, you should have Node.js 16.17.0 or greater installed on your machine.

I recommend using either [nvm](https://github.com/nvm-sh/nvm) or [fnm](https://github.com/Schniz/fnm) to manage Node.js versions.

### Creating and configuring the project

To get started, let's first create the Next.js application, configure it, and install the dependencies.

```sh
npx create-next-app full-stack-arweave
```

Change into the new directory and install the following dependencies:

```sh
npm install warp-contracts react-markdown uuid
```

### Configuring the Next.js app

Open `package.json` and add the following configuration:

```json
"type": "module",
```

Then update `next.config.js` to use ES Modules to export the `nextConfig`:

```javascript
/* replace */
module.exports = nextConfig

/* with this*/
export default nextConfig
```

This will enable the Next.js app to use ES Modules.

Next, add the following to the `.gitignore` file:

```
wallet.json
testwallet.json
transactionid.js
```

> Never push wallet information to any public place like GitHub. In this tutorial we'll only be working with `testnet`, but we'll have the code available for you to push to `mainnet`. Just in case, we're adding `wallet.json` to `.gitignore`.

## Warp Contracts

Next, let's create and test out the smart contracts.

### About Smartweave contracts

Smartweave contracts work like this.

__1.__ An initial state for the application is defined as a JSON object.

Some basic initial state might look something like this for a counter application that increments and decrements a number:

```json
{
"counter" : 0
}
```

__2.__ The logic of a Smartweave contract is written in a function named `handle`.

This function defines different actions that can be called on the contract, which manipulate the state. Actions are similar to functions in a normal smart contract or program. Each action will update the state in some way.

A basic handler for a counter that uses the above state might look something like this:

```javascript
export function handle(state, action) {
  if (action.input.function === 'increment') {
    state.counter += 1
  }
  if (action.input.function === 'decrement') {
    state.counter -= 1
  }
  return { state }
}
```

In this handler, there are two actions - `increment` or `decrement`. The logic here is pretty straightforward.

__3.__ To update the state of the contract, we can call [`writeInteraction`](https://github.com/warp-contracts/warp#writeinteraction) from the Warp SDK.

Here's a basic example of how this might look when calling this function on the server:

```javascript
import { WarpFactory } from 'warp-contracts'
const transactionId = "BA3EIfkKvlPXLk5sEN8loAmp2zr0MezSPhwaujTNli8"
import wallet from './wallet.json'

let warp = WarpFactory.forLocal()
const contract = warp.contract(transactionId).connect(wallet)
await contract.writeInteraction({
  function: "decrement"
})
```

Then, we can read the state at any time:

```javascript
const contract = warp.contract(transactionId).connect();
const { cachedValue } = await contract.readState();
```

## Writing the contract

Now that we have a basic understanding of how contracts work, let's start writing some code.

In the root of the project, create a new folder named `warp`.

In this folder, create a new file named `contract.js`:

```javascript
/* warp/contract.js */
export function handle(state, action) {
  /* address of the caller is available in action.caller */
  if (action.input.function === 'initialize') {
    state.author = action.caller
  }
  if (action.input.function === 'createPost' && action.caller === state.author) {
    const posts = state.posts
    posts[action.input.post.id] = action.input.post
    state.posts = posts
  }
  if (action.input.function === 'updatePost' && action.caller === state.author) {
    const posts = state.posts
    const postToUpdate = action.input.post
    posts[postToUpdate.id] = postToUpdate
    state.posts = posts
  }
  if (action.input.function === 'deletePost' && action.caller === state.author) {
    const posts = state.posts
    delete posts[action.input.post.id]
    state.posts = posts
  }
  return { state }
}
```

This is the contract for our blogging app.

We have functions to __create__, __update__, and __delete__ a post ([CRUD](https://www.dronahq.com/crud-app/#:~:text=CRUD%20apps%20are%20the%20user,user%20interface%2C%20and%20the%20APIs)). We also have an `initialize` function which adds a basic authorization rule that only allows the blog owner to call any of these functions by setting the contract deployer as the owner.

Next, create a file in the `warp` directory named `state.json` and add the following JSON:

```json
{
  "posts": {},
  "author": null
}
```

Here, we have an initial state of `posts` set to an empty object, and `author` set to null.

We're done with our contract, now let's write the code to deploy, update, and read the state of the contract.

### Deploy, update, and read

Next, create a new file named `configureWarpServer.js` in the `warp` directory.

```javascript
import { WarpFactory } from 'warp-contracts'
import fs from 'fs'

/*
*  environment can be 'local' | 'testnet' | 'mainnet' | 'custom';
*/

const environment = process.env.WARPENV || 'testnet'
let warp

if (environment === 'testnet') {
  warp = WarpFactory.forTestnet()
} else if (environment === 'mainnet') {
  warp = WarpFactory.forMainnet()
} else {
  throw Error('environment not set properly...')
}

async function configureWallet() {
  try {
    if (environment === 'testnet') {
      /* for testing, generate a temporary wallet */
      try {
        return JSON.parse(fs.readFileSync('../testwallet.json', 'utf-8'))
      } catch (err) {
        const { jwk } = await warp.generateWallet()
        fs.writeFileSync('../testwallet.json', JSON.stringify(jwk))
        return jwk
      }
    } else if (environment === 'mainnet') {
      /* for mainnet, retrieve a local wallet */
      return JSON.parse(fs.readFileSync('../wallet.json', 'utf-8'))
    } else {
      throw Error('Wallet not configured properly...')
    }
  } catch (err) {
    throw Error('Wallet not configured properly...', err)
  }
}

export {
  configureWallet,
  warp
}
```

In this file we're configuring `warp` server based on whether we're in a `testing` or `mainnet` (production) environment.

We then have a function that configures the wallet we'll be using to deploy the contract. If we're testing, we can just spin up a test wallet automatically using `generateWallet`. If we're in production, we have the option of importing a wallet locally.

Now that we have the ability to configure a wallet and a Warp server, let's create the function for deploying the contracts.

### Deploying the contract

Create a new file named `deploy.js` in the `warp` directory with the following code:

```javascript
import fs from 'fs'
import { configureWallet, warp } from './configureWarpServer.js'

async function deploy() {
  const wallet = await configureWallet()
  const state = fs.readFileSync('state.json', 'utf-8')
  const contractsource = fs.readFileSync('contract.js', 'utf-8')

  const { contractTxId } = await warp.createContract.deploy({
    wallet,
    initState: state,
    src: contractsource
  })
  fs.writeFileSync('../transactionid.js', `export const transactionId = "${contractTxId}"`)

  const contract = warp.contract(contractTxId).connect(wallet)
  await contract.writeInteraction({
    function: 'initialize'
  })
  const { cachedValue } = await contract.readState()

  console.log('Contract state: ', cachedValue)
  console.log('contractTxId: ', contractTxId)
}

deploy()
```

The `deploy` function will deploy the contract to Arweave and write the transaction id to the local file system.

### Reading the state

Next, let's create a file named `read.js` with the following code:

```javascript
import { warp, configureWallet } from './configureWarpServer.js'
import { transactionId } from '../transactionid.js'

async function read() {
  let wallet = await configureWallet()
  const contract = warp.contract(transactionId).connect(wallet);
  const { cachedValue } = await contract.readState();

  console.log('Contract state: ', JSON.stringify(cachedValue))
}
read()
```

## Writing an update

The last function we'll write is for creating a new post.

In the `warp` directory, create a new file named `createPost.js` with the following code:

```javascript
import { warp, configureWallet } from './configureWarpServer.js'
import { transactionId } from '../transactionid.js'
import { v4 as uuid } from 'uuid'

async function createPost() {
  let wallet = await configureWallet()
  const contract = warp.contract(transactionId).connect(wallet)
  await contract.writeInteraction({
    function: "createPost",
    post: {
      title: "Hi from first post!",
      content: "This is my first post!",
      id: uuid()
    }
  })
}

createPost()
```

## Testing it out

Now we can test everything out.

To deploy the contract, run the following command from the `warp` directory:

```sh
node deploy
```

This should deploy the contract to testnet.

> Once the contract has been deployed, you can use the [__Sonar block explorer__](https://sonar.warp.cc/) to view the contract and the current state of it. The contract transaction ID will be available in `transactionid.js`. Be sure to switch to __testnet__ to view the contract from this deployment.

![Sonar Transaction](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lvsuj7vptgey1d7p52q8.jpg)

Next, let's read the current state:

```sh
node read
```

The returned state of the contract should look something like this:

```
{"state":{"posts":{},"author":"-YzqAM_VDCqFZEk6iZ3B8Y-b6SxHoh0F1SvjOCW49nY"},"validity":{"36CmMGSlrGNvvCCfldtiUza4ZnQ9_bFW0YoEh8NCVe0":true},"errorMessages":{}}
```

Now, let's create a post:

```sh
node createPost
```

Now, when we read the updated state, we should see the new post in the updated state:

```sh
node read
```

## Building out the web app

Now that we understand how to deploy and test out a Smartweave contract with Warp, let's build out a front-end application that will interact with it and use it.

Since the application we're building is a blog, we'll need to create two basic views:

1. A view to see the posts created by the user.
2. A view to allow users to create posts.

We'll also need a file to hold a function we'll be using to configure `warp` for the client (similar to how we configured warp for the server previously).

Create a new file named `configureWarpClient.js` in the root of the app and add the following code:

```javascript
import { WarpFactory } from 'warp-contracts'
import { transactionId } from './transactionid'
import wallet from './testwallet'

/*
*  environment can be 'local' | 'testnet' | 'mainnet' | 'custom';
*/

const environment = process.env.NEXT_PUBLIC_WARPENV || 'testnet'
let warp
let contract

async function getContract() {
  if (environment == 'testnet') {
    warp = WarpFactory.forTestnet()
    contract = warp.contract(transactionId).connect(wallet)
  } else if (environment === 'mainnet') {
    warp = WarpFactory.forMainnet()
    contract = warp.contract(transactionId).connect()
  } else {
    throw new Error('Environment configured improperly...')
  }
  return contract
}

export {
  getContract
}
```

### Creating a post

Next, in the `pages` directory create a new file named `create-post.js` and add the following code:

```javascript
import { useState } from 'react'
import { getContract } from '../configureWarpClient'
import { v4 as uuid } from 'uuid'
import { useRouter } from 'next/router'

export default function createPostComponent() {
  const [post, updatePost] = useState({
    title: '', content: ''
  })
  const router = useRouter()

  async function createPost() {
    if (!post.title || !post.content) return
    post.id = uuid()
    const contract = await getContract()
    try {
      const result = await contract.writeInteraction({
        function: "createPost",
        post
      })
      console.log('result:', result)
      router.push('/')
    } catch (err) {
      console.log('error:', err)
    }
  }
  return (
    <div style={formContainerStyle}>
      <input
        value={post.title}
        placeholder="Post title"
        onChange={e => updatePost({ ...post, title: e.target.value})}
        style={inputStyle}
      />
      <textarea
        value={post.content}
        placeholder="Post content"
        onChange={e => updatePost({ ...post, content: e.target.value})}
        style={textAreaStyle}
      />
      <button style={buttonStyle} onClick={createPost}>Create Post</button>
    </div>
  )
}

const formContainerStyle = {
  width: '900px',
  margin: '0 auto',
  display: 'flex',
  flexDirection: 'column',
  alignItems: 'flex-start'
}

const inputStyle = {
  width: '300px',
  padding: '8px',
  fontSize: '18px',
  border: 'none',
  outline: 'none',
  marginBottom: '20px'
}

const buttonStyle = {
  width: '200px',
  padding: '10px 0px'
}

const textAreaStyle = {
  width: '100%',
  height: '300px',
  marginBottom: '20px',
  padding: '20px'
}
```

### Reading and displaying posts

Next, update `pages/index.js` with the following code:

```javascript
import { useEffect, useState } from 'react'
import { getContract } from '../configureWarpClient'
import ReactMarkdown from 'react-markdown'

export default function Home() {
  const [posts, setPosts] = useState([])
  useEffect(() => {
    readState()
  }, [])
  async function readState() {
    const contract = await getContract()
    try {
      const data = await contract.readState()
      console.log('data: ', data)
      const posts = Object.values(data.cachedValue.state.posts)
      setPosts(posts)
      console.log('posts: ', posts)
    } catch (err) {
      console.log('error: ', err)
    }
  }

  return (
    <div style={containerStyle}>
      <h1 style={headingStyle}>PermaBlog</h1>
      {
        posts.map((post, index) => (
          <div key={index} style={postStyle}>
            <p style={titleStyle}>{post.title}</p>
            <ReactMarkdown>
              {post.content}
            </ReactMarkdown>
          </div>
        ))
      }
    </div>
  )
}

const containerStyle = {
  width: '900px',
  margin: '0 auto'
}

const headingStyle = {
  fontSize: '64px'
}
const postStyle = {
  padding: '15px 0px 0px',
  borderBottom: '1px solid rgba(255, 255, 255, .2)'
}

const titleStyle = {
  fontSize: '34px',
  marginBottom: '0px'
}
```

## Navigation

Next, update `pages/_app.js` 

```javascript
import '../styles/globals.css'
import Link from 'next/link'

function MyApp({ Component, pageProps }) {
  return (
    <div>
      <nav style={navStyle}>
        <Link href="/" style={linkStyle}>
          Home
        </Link>
        <Link href="/create-post" style={linkStyle}>
          Create Post
        </Link>
      </nav>
      <Component {...pageProps} />
    </div>
  )
}

const navStyle = {
  padding: '30px 100px'
}

const linkStyle = {
  marginRight: '30px'
}

export default MyApp
```

### Testing it out

Now let's run the app and test it out:

```sh
npm run dev
```

When the app loads, the post created on the server should be rendered in the UI.

Next, create a post. If the post is successfully created, it should show up in the list of posts on the home page.

## Next Steps

### Deploying to mainnet

If you'd like to deploy and connect to Arweave mainnet, follow these steps:

1. Download [ArConnect wallet](https://www.arconnect.io/)

2. Request AR tokens from faucet, purchase them from an exchange, or swap at an exchange like [changeNOW](https://changenow.io/)

3. Download new wallet into a file named `wallet.json`. __Be sure to add this file to your `.gitignore` and never ever make public or push to Git__.

4. Set local environment variable to `mainnet` in the terminal session you will be deploying from :

    ```sh
    export WARPENV=mainnet
    ```


5. Create `.env.local` file in the root of the app and add the following environment variable:

    ```sh
    NEXT_PUBLIC_WARPENV=mainnet
    ```


6. Deploy the contract from the `warp` directory:

    ```sh
    node deploy
    ```


7. Run the app

    ```sh
    npm run dev
    ```

### Learning resources

If you'd like to dive deeper and learn more about Warp, Smartweave, and Arweave, check out [Warp Academy](https://academy.warp.cc/)