Creating and querying tags with Arweave

Creating and querying tags with Arweave

Introduction

Arweave allows developers to add specific tags to their transactions, giving us the ability to attach unique identifiers in key, value pair format to each of our transactions. One of the benefits of this is giving us a way to query certain tags, returning only the specified data attached to those tags. In this walkthrough we will be building an Arweave GIF uploader, attaching tags to the GIF transaction and querying those targeted uploads to display.

Considering each transaction has its own metadata, having a way to write custom tags and queries gives flexibility to create user friendly interfaces using Arweave transaction data.

In this build we will be building functions for connecting and disconnecting our Arweave web wallet, uploading GIF's and querying using the tags we add to the transactions of those uploads.

Screen Shot 2022-09-05 at 5.49.05 PM.png

If you are new to GraphQL and Arweave, here is an introduction article that might help you understand more of what we're doing here.

Getting Started

For this we will be using NextJS, arweave-js and ArConnect web wallet. Lets create a Next app by running npx create-next-app APP_NAME and change into that directory and open the code in our editor.

Next we want to add the Arweave SDK by running yarn add arweave, as we will be using it to create our transactions, upload the GIF's and run our GraphQL queries against to retrieve our data. You will also need an ArConnect, which is an Arweave browser wallet.

All of our work will be inside of the index.js file. Browse through the code below 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>

      <footer className={styles.footer}>

      </footer>
    </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")

Screen Shot 2022-09-09 at 9.36.42 AM.png

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.

Screen Shot 2022-09-05 at 11.40.21 AM.png

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!

Conclusion

With Arweave's "pay once, store forever" model, the option to add tags to transactions and GraphQL queries, we are able to create useful interfaces for users to not only interact with but to upload and own their data permanently.

With these tools there are many benefits of using these tags in our dApps. Giving direct access to transactions stored with the same tags is one of many more. Examples of this may be a photo album linked to a user's social profile, or querying a particular smart contract for metadata. The possibilities are plentiful and permissionless.