The Permaweb

Built on top of the Arweave network is the Permaweb, a one of a kind, global and community owned decentralized web, with all of its content being permanent. With Arweave you pay once to deploy applications to the Permaweb and have them operating the way they were originally intended, forever. No single point of failure or surprise additional payments. Zero maintenance and downtime.

The Permaweb's serverless structure optimizes network utilization, which auto scales depending on the required capacity needed. You only pay for the required storage and do not pay for unused space. Miners in the network are incentivized by earning tokens by providing unused space to store the data permanently. This globally distributed network ensures the storage of data without compromising privacy. Without permanent data, applications cannot run without a centralized controller, which is why we are able to build and deploy censor resistant and permissionless applications. "Code is law" but for web applications, where consumer integrity is guaranteed.

In the rest of this article we will build what will start out as a static site deployment followed by adding dynamic elements that interact with the Arweave network. Then in a separate build we will work with Smartweave smart contracts and Arlocal to test them. The static, dynamic and Smartweave applications will be deployed to the Permaweb.

Static site deployment

Deploying static and dynamic sites to the Permaweb is straightforward using tools like Arkb. All you need is a funded Arweave wallet, which you can get from here and a statically generated build of your site.

Arkb is a command line tool for Arweave that can be used to upload files and in this case, the static build directory of an application. One line deployments. No size limit, file limit or duplicate file uploads.

To use Arkb we will create a NextJS project, though you can use any JavaScript framework that will statically generate. First run npx create-next-app zero-to-arweave && cd zero-to-arweave followed by opening the app in your editor. Then we will need to install the Arkb CLI by running either yarn global add arkb or npm install -g arkb in your terminal.

Inside the index.js file we can add a button along with an onClick function just to test it out when we deploy. Replace the contents of index.js with the following:

import Head from 'next/head'
import styles from '../styles/Home.module.css'

export default function Home() {
  const handleClick = () => {
    alert("CLICKED")
  }
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <button onClick={handleClick}>Click Me</button>
      </main>
    </div>
  )
}

Now that we're set up we can get started on the configuration for the deployment. We need to add a few things to the next.config.js for it to build successfully, so yours should look like this

Click to view next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
  trailingSlash: true,
  exportPathMap: function () {
    return {
      "/": { page: "/" }
    };
  },
  assetPrefix: './',
}

module.exports = nextConfig

Inside your package.json, in the build command add next build && next export.

Run yarn build to generate your static files/directories. You will notice the directory named out. With the command below we are passing in the out directory, path to wallet, and telling Arkb to bundle our transaction. If the transaction is not bundled, the files will be deployed separately and the build will not register and display your app.

arkb deploy out --wallet /PATH_TO_WALLET.json --bundle

If your transaction was successful, the deployment link will be displayed in your CLI and after a few minutes the site will be available to use on the Permaweb.

Dynamic site deployment

The Permaweb supports dynamic sites, allowing us to use transaction data and its tags to add query the network. Adding wallet to wallet transactions, profit sharing communities, and Atomic Assets, along with utilizing tools like GraphQL and Smartweave contracts, we are able to create and deploy dynamic dApps to the Permaweb.

The Permaweb connects the dots for the decentralized web, but not only with Arweave components, it allows ways to integrate and communicate with other networks using tools like Bundlr. Its composability opens doors for developers to make useful and truly decentralized sites that will stand the test of time, presented only as they are programmed to be, forever.

In this build we are using Arconnect to upload GIFs to Arweave, and querying them in the UI. As all of our work will be inside of the index.js file, replace everything we previously deployed with the code block below. Browse through the code to see how it works. Later we will explore what each of the functions do.

Click to view code
import Head from 'next/head'
import { useEffect, useState } from 'react';
import styles from '../styles/Home.module.css'
import Image from 'next/image'
import Arweave from 'arweave';

export default function Home() {
  const [selectedFile, setSelectedFile] = useState();
  const [img, setImg] = useState();
  const [message, setMessage] = useState({});
  const [currentWallet, setCurrentWallet] = useState();
  const [gifs, setGifs] = useState();

  const arweave = Arweave.init({
    host: 'arweave.net',
    port: 443,
    protocol: 'https',
    timeout: 3000000
  });

  const getGifs = async (wallet) => {
    const queryWallet = wallet !== undefined ? wallet : currentWallet;
    const gifs = await arweave.api.post('graphql',
      {
        query: `query {
          transactions(
            owners: ["${queryWallet}"]
            tags: [{
                name: "App-Name",
                values: ["PbillingsbyGifs"]
              },
              {
                name: "Content-Type",
                values: ["image/gif"]
              }]
          ) {
            edges {
                node {
                id
                    tags {
                  name
                  value
                }
                data {
                  size
                  type
                }
              }
            }
          }
        }`})
    setGifs(gifs.data.data.transactions.edges)
  }

  const fetchWallet = async () => {
    const permissions = await window.arweaveWallet.getPermissions()
    if (permissions.length) {
      const wallet = await window.arweaveWallet.getActiveAddress();
      setCurrentWallet(wallet);
    }
  }

  useEffect(() => {
    fetchWallet()
      .catch(console.error);

    getGifs(currentWallet);
  }, [currentWallet])

  const connect = async () => {
    await arweaveWallet.connect('ACCESS_ADDRESS');
    setMessage({
      message: '...connecting',
      color: 'yellow'
    })

    const wallet = await arweaveWallet.getActiveAddress();

    setCurrentWallet(wallet);
    getGifs(wallet);
    setMessage({})
  }

  const disconnect = async () => {
    await arweaveWallet.disconnect();
    window.location.reload();
  }

  const handleFileChange = (e) => {
    const reader = new FileReader();
    const file = e.target.files[0];

    if (file.type !== 'image/gif') {
      setMessage({ message: "File must be a gif", color: "red" });
      return;
    }

    if (file) {
      setMessage({})
      reader.onloadend = () => {
        if (reader.result) {
          setSelectedFile(Buffer.from(reader.result));
        }
      };
      reader.readAsArrayBuffer(file);
      const objectUrl = URL.createObjectURL(file);
      setImg(objectUrl);
    }

  }

  const uploadGif = async () => {
    try {
      const tx = await arweave.createTransaction({
        data: selectedFile
      }, 'use_wallet');

      tx.addTag("Content-Type", "image/gif");
      tx.addTag("App-Name", "PbillingsbyGifs");

      await arweave.transactions.sign(tx, 'use_wallet');
      setMessage({
        message: 'Uploading to Arweave.',
        color: 'yellow'
      })

      const res = await arweave.transactions.post(tx);

      setMessage({
        message: 'Upload successful. Gif available after confirmation.',
        color: 'green'
      })
      getGifs()
    }
    catch (message) {
      console.log('message with upload: ', message);
    }
  }

  return (
    <div className={styles.container}>
      <Head>
        <title>YourGifs</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <div>
        {currentWallet ?
          <div>
            <p>Owner: {currentWallet}</p>
            <button onClick={disconnect}>Disconnect</button>
          </div> :
          <button onClick={connect}>Connect to view uploaded gifs</button>

        }
        <div style={{ textAlign: 'center', maxWidth: '25rem', margin: '0 auto', height: '25rem' }} className="main">
          <input type="file" onChange={handleFileChange} />
          {message && <p style={{ color: message.color }}>{message.message}</p>}
          {img &&
            <div>
              {selectedFile && (
                <div>
                  <Image src={img} width={250} height={250} alt="local preview" /><br />
                  <p><button onClick={() => uploadGif(selectedFile)}>Upload GIF</button></p>
                </div>
              )
              }
            </div>
          }
        </div>
        <div>
          {gifs && <p align="center">Gifs: {gifs.length}</p>}
          <div style={{ display: 'flex', overflow: 'scroll', maxWidth: '40vw', margin: '0 auto', border: '1px solid #eee' }}>
            {gifs && gifs.map(gif => {
              return <div key={gif.node.id} style={{ margin: '2rem' }}>
                <a href={`https://arweave.net/${gif.node.id}`} target="_blank" rel="noreferrer">
                  <img src={`https://arweave.net/${gif.node.id}`} style={{ maxWidth: '10rem' }} />
                </a>
              </div>
            })}
          </div>
        </div>
      </div>
    </div >
  )
}

Connecting to Arweave

The first thing to do is create an Arweave gateway. We set the values inside of the Arweave.init function call to specify what gateway we want to connect to. For this build we are interacting with the arweave.net gateway (mainnet).

 const arweave = Arweave.init({
    host: 'arweave.net',
    port: 443,
    protocol: 'https',
    timeout: 3000000
  });

Here we have the connect and disconnect functions. The connect function does 3 things: Connects to our Arweave web wallet, sets currentWallet to that value and fetches all of that addresses GIF transactions using the tags we create later in our uploadGif function.

  const connect = async () => {
    await arweaveWallet.connect('ACCESS_ADDRESS');
    setMessage({
      message: '...connecting',
      color: 'yellow'
    })

    const wallet = await arweaveWallet.getActiveAddress();

    setCurrentWallet(wallet);
    getGifs(wallet);
    setMessage({})
 }

 const disconnect = async () => {
    await arweaveWallet.disconnect();
    window.location.reload();
 }

To prevent us having to reconnect every time we refresh the application, we can use functions from ArConnect to check if we've given permissions to be connected. We then use the useEffect hook to run these checks and fetch the GIF's from that particular wallet address.

const fetchWallet = async () => {
    const permissions = await window.arweaveWallet.getPermissions()
    if (permissions.length) {
      const wallet = await window.arweaveWallet.getActiveAddress();
      setCurrentWallet(wallet);
    }
  }

  useEffect(() => {
    fetchWallet()
      .catch(console.error);

    getGifs(currentWallet);
  }, [currentWallet])

Uploading to Arweave

Now that we're connected to the Arweave gateway and our wallet connection complete, we handle the GIF uploads.

On the input element for the file, an onChange event listener passes the file input to handleFileChange function that first checks if the file type is image/gif, if true, it will set the variable selectedFile to equal to a Uint8Array.

If the input type isn't image/gif, an error will display and we won't be able to upload that file.

 const handleFileChange = (e) => {
    const reader = new FileReader();
    const file = e.target.files[0];

    if (file.type !== 'image/gif') {
      setMessage({ message: "File must be a gif", color: "red" });
      return;
    }

    if (file) {
      setMessage({})
      reader.onloadend = () => {
        if (reader.result) {
          setSelectedFile(Buffer.from(reader.result));
        }
      };
      reader.readAsArrayBuffer(file);
      const objectUrl = URL.createObjectURL(file);
      setImg(objectUrl);
    }
  }

Once we have the value of selectedFile, we are ready to upload the GIF. In the uploadGif function, we first create the transaction and assign selectedFile to the data attribute. Once the transaction has been created we add our tags which we later use in the GraphQL query to retrieve all of our GIF's. The specific tags we want to target are created in this line tx.addTag("App-Name", "PbillingsbyGifs")

We then use the sign function on the transaction, which takes in a transaction and wallet key. In this walkthrough we are using a browser wallet so we pass in use_wallet which triggers a prompt to enter the password to give permissions to sign the transaction. Once we authorize the transaction it gets posted to the Arweave network.

const uploadGif = async () => {
    try {
      const tx = await arweave.createTransaction({
        data: selectedFile
      });

      tx.addTag("Content-Type", "image/gif");
      tx.addTag("App-Name", "PbillingsbyGifs");

      await arweave.transactions.sign(tx, 'use_wallet');

      setMessage({
        message: 'Uploading to Arweave.',
        color: 'yellow'
      })

      const res = await arweave.transactions.post(tx);

      setMessage({
        message: 'Upload successful. Gif available after confirmation.',
        color: 'green'
      })
      getGifs()
    }
    catch (message) {
      console.log('message with upload: ', message);
    }
  }

Querying using tags

Once we have uploaded some transactions with tags attached we can use GraphQL queries to retrieve that data and display it in our UI. For this particular query we are specifying a single address to query its transactions with the relevant tags.

Below in the getGifs we use our variable currentWallet to pass the wallet address we are using along with the tags associated to the transactions we made in uploadGif into the query. If any results are returned it will set the variable gifs to an array of objects. You can retrieve transactions from more than one wallet by adding an array of wallet address as comma separated strings. To get all the transactions from a particular tag, remove the owners key/value from the query.

In this case we are querying the tag App-Name for all of the GIF's I uploaded making this walkthrough, which is PbillingsbyGifs. If you change this to your own tags the returned values will be your uploaded files.

const getGifs = async () => {
    const gifs = await arweave.api.post('graphql',
      {
        query: `query {
          transactions(
            owners: ["${currentWallet}"]
            tags: [{
                name: "App-Name",
                values: ["PbillingsbyGifs"]
              },
              {
                name: "Content-Type",
                values: ["image/gif"]
              }]
          ) {
            edges {
                node {
                id
                    tags {
                  name
                  value
                }
                data {
                  size
                  type
                }
              }
            }
          }
        }`})
    setGifs(gifs.data.data.transactions.edges)
  }

If one or more transactions have been uploaded, the blocks will need to be confirmed before we will be able to query them, which can take up to a few minutes. Once confirmed, we will now have some GIFs to display in our UI!

Deploying to the Permaweb

This step is the same as the one in the previous "Static Deployment" section. Run yarn build in your terminal and then arkb deploy out --wallet ../PATH_TO_WALLET.json --bundle to deploy.

Smartweave and the Web of Value

The Arweave community continues to solve unique problems with innovative solutions using Smartweave smart contracts. With all the use cases for Arweave technology, Smartweave ties it all together giving developers the ability to create scaleable, multifaceted applications where each contract has a programmable set of rules.

With Arweave the Web of Value is how creators are rewarded, not by ad revenue streams, but value provided to the ecosystem which is decided on by Permaweb users. When users consume and pay attribution to content, media, images, and blog posts, those streams are created by stamps and investment. The "Stamps" show the value of Permaweb content, or a consumer may purchase a percentage of the content in the reward of the value shared. This ensures that the quality of content is decided on by the community, as opposed to the traditional ways of pushing content via advertisting algorithms and the highest bidding content provider. The Web of Value is created to serve the content creators and consumers, and not a third party looking to use their resources and money to take over.

Trading exchanges, decentralized NoSQL databases, Arweave verification services and profit sharing tokens/communities are only just scratching the surface of whats possible with Smartweave contracts.

The contracts are similar to the ones we know from other blockchains, though they use lazy-evaluation to shift contract computation from the network to user for validation. The user evaluates their call and then writes the updated state to the Arweave network. This process is repeated, where new users validate each other’s transactions and adding their own state result. Arweave acts as the generic data consensus and sharing layer, while users verify transactions on the contracts they interact with.

In this next build we are going to create a basic Smartweave contract using Warp Contracts, a UI to interact with the contract, test it with Arlocal, and deploy it with Arkb.

The Setup

We will use a similar configuration as our last build so add the next.config.js from the previous build along with adding "build": "next build && next export" and "deploy": "node contract/deploy.mjs" to our package.json.

We can add the dependencies by running yarn add arweave warp-contracts dotenv

At the root of your project directory add your Arweave wallet.json file, along creating a file named .env and inside create a variable called WARP and set the value to mainnet. We will be deploying a contract to mainnet and using the transaction ID for our UI. In your .gitignore add .env and wallet.json.

Be careful and make sure you don't push your wallet to GitHub!!!

The Contract

The contract for this section is an introduction to Arweave smart contracts. It is a basic counter contract that increments the clicks state by 1 when it is interacted with. These contracts can be written in JavaScript, Rust, Solidity, C, and more.

Click to view contract
const functions = { click }

export function handle(state, action) {
  if (Object.keys(functions).includes(action.input.function)) {
    return functions[action.input.function](state, action)
  }
  throw new ContractError('function not defined!')
}

function click(state, action) {
  state.clicks = state.clicks + 1;
  return { state }
}

Here we declare an object, functions, where we will store the functions created in the contract. The handle function is where our interactions are triggered. This function's parameters are state and action which are passed in as arguments to the contract when it is created or interacted with. In the action argument is a caller value which is the Arweave address of the interacting wallet.

STATE:  { clicks: 1 }
ACTION:  {
  input: { function: 'click' },
  caller: 'Ub5LhZ_QzExXP3t_FOpeC9YYYra00mQN33x6Wez4XcU'
}

In the click function it updates and returning the state.

The deploy function

This is where the contract gets instantiated and deployed. The first things this function does is checks if we are using our local environment.

For test - local instance of the Warp gateway and a test wallet. For mainnet - Mainnet Warp gateway and Arweave wallet.

Once we have we get the contractSource which we wrote in the previous section, which we need to pass into Warp's createContract function along with the wallet we just declared, and our contracts initial state, which for this contract is JSON.stringify({"clicks": 0}). For contracts with more key/value pairs for its initial state, a separate JSON file can be created and imported to pass into initState.

import fs from 'fs'
import { WarpFactory } from 'warp-contracts'
import * as dotenv from 'dotenv'
dotenv.config()

export async function deploy() {
  const isLocal = process.env.WARP === "local"
  const warp = isLocal ? WarpFactory.forLocal() : WarpFactory.forMainnet();
  let wallet
  if (isLocal) {
    const testWallet = await warp.testing.generateWallet()
    wallet = testWallet.jwk
  }
  else {
    wallet = JSON.parse(fs.readFileSync('wallet.json', 'utf-8'))
  }
  const contractSource = fs.readFileSync('contract/contract.js', 'utf-8')

  const { contractTxId } = await warp.createContract.deploy({
    wallet,
    initState: JSON.stringify({
      "clicks": 0
    }),
    src: contractSource
  })

  const contract = warp.contract(contractTxId).connect(wallet)

  const { cachedValue } = await contract.readState()
  console.log("STATE: ", cachedValue)
  console.log("CONTRACT ID: ", contractTxId)
  
  return { contractTxId, cachedValue }
}
deploy()

We are then calling on our Warp gateway to createContract and deploy it, whilst abstracting and declaring the contractTxId which we will use to retrieve the contract, read its state and set cachedValue so we can confirm the status and state of the deployment.

Run yarn deploy in your terminal. Be sure to take note of the contractTxId as we will use this in our UI to interact with that contract.

The UI

The interface for this is a simple page with 2 buttons, one that reads state using the getClicks function we create and displays the click count, and the other allows us to update the state by calling addClick, which then triggers Warps writeInteraction function on the contract. Replace CONTRACT_SRC with your contractTxId from the previous section.

Click to view frontend
import Head from 'next/head'
import styles from '../styles/Home.module.css'
import { useState } from 'react'
import { WarpFactory } from 'warp-contracts'

const CONTRACT_SRC = "YOUR_CONTRACT_ID"

export default function Home() {
  const [clicks, setClicks] = useState();
  const warp = WarpFactory.forMainnet();
  const contract = warp.contract(CONTRACT_SRC).connect();

  const getClicks = async () => {
    const { cachedValue } = await contract.readState();
    setClicks(cachedValue.state.clicks)
  }

  const addClick = async () => {
     try {
      setClicks('updating...')
      await contract.writeInteraction({
        function: "click",
      })
      getClicks()
    }
    catch (error) {
      console.log(error)
    }
  }

  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <div style={{ display: 'flex', gap: '1rem' }}>
          <button onClick={getClicks}>Get clicks</button>
          <button onClick={addClick}>Add View</button>
        </div>
        <div>
          {clicks && <p>{clicks} clicks</p>}
        </div>
      </main>
    </div>
  )
}

addClick will trigger your Arweave web wallet modal to sign the transaction.

Once signed the state will increment by 1 and display the new state.

Arlocal and testing

Testing your transactions and smart contracts on mainnet can be expensive, so there are a few tools in the Arweave ecosystem to help developers test their applications locally before deploying to the Permaweb. Arlocal is a tool by Textury built for running local Arweave gateway-like servers. It is use by developers for local testing of Arweave interactions, using generated wallets for these transactions.

Arlocal has a CLI you can use, though we will be using it as a library by installing the package and importing Arlocal. Lets look at a basic test of the smart contract we created in the previous section. For this test we will be using uvu, but Arlocal works similarly with other test suites too.

The first thing we need to do for this test is change our WARP environment variable in .env to local. Then install the test dependencies by running yarn add arlocal uvu -D. Then import WarpFactory for generating our test wallet and connecting to our contract to test the second interaction, followed by our test suite, Arlocal and our deploy function we created in the previous section.

Add "test": "uvu ./tests" to your package.json. This will be used to run our test.

import { WarpFactory } from 'warp-contracts'
import { test } from 'uvu'
import * as assert from 'uvu/assert'
import ArLocal from 'arlocal'
import { deploy } from '../contract/deploy.mjs'

const arlocal = new ArLocal.default()

test('create contract and test state update', async () => {
    await arlocal.start()

    const res = await deploy()

    assert.is(res.cachedValue.state.clicks, 0)

    const warp = WarpFactory.forLocal()
    const wallet = await warp.testing.generateWallet()
    const contract = warp.contract(res.contractTxId).connect(wallet.jwk)

    await contract.writeInteraction({
        function: 'click'
    })

    const secondCall = await contract.readState()
    assert.is(secondCall.cachedValue.state.clicks, 1)

    await arlocal.stop()
})

test.run();

The Test

After importing whats needed for testing, the first thing we are doing is declaring an arlocal variable and setting its value to be a new Arlocal instance. This is needed for us to call the async start and stop functions on that instance which is responsible for creating and tearing down the testing environment at the start and end of each test case.

The deploy function is called which will create a smart contract and return a contract ID and cachedValue, which is an object that returns the contract state, block validity, and any errors returned from the deployment.

Next we use WarpFactory to create a local instance to generate a wallet which will be used for our second contract interaction in the test, which will be checking if it correctly updates our state counter. warp.contract(res.contractTxId).connect(wallet.jwk) is using the Warp instance to retrieve the contract we created. Once returned, the second contract interaction is triggered followed by reading the state of that contract to see if the state value has incremented.

Once that last interaction is complete, the testing environment will be torn down, removing the Arlocal instance.

Conclusion

The ever growing suite of tools in the Arweave ecosystem gives developers more leverage to build complex and flexible dApps deployed to the Permaweb. Pay once and rest assured knowing that your dApps will run the exact same way as originally intended.

Some examples of dApps using the Arweave tools we just learned about -