A worldwide map location and decentralized blog dapp on ArWeave!
This projects uses common web techonologies and being built on the Arweave Blockweave
PinPoint Earth
PinPoint Earth is an innovative mobile application designed to help users explore, navigate, and interact with the world around them, users can take photos and document a moment at a specific location, the decentralized blog post feature to write documemtations and articles etc. The app leverages advanced mapping technology and location-based services to provide a wide range of features and functionalities.
A little background on Arweave
Arweave stands out due to its distinctive role as both our storage and application service, operating in a decentralized manner. In our application, we utilize Arweave for data reading and writing, as well as deploying the application's code onto Arweave, enabling app delivery through web browsers. While this concept might initially require some comprehension, Arweave functions through nodes and gateways. The nodes oversee storage and blockweave aspects, whereas the gateways manage Cache, RPC API, and Query functionalities. It's within the gateway that the true magic happens for our decentralized applications. Through metadata tags like 'Content-Type,' data storage allows us to guide gateway servers in data presentation. Consequently, a web browser can seamlessly interact with our application, almost as if the gateway were a web server.
Arweave operates on a blockweave architecture, akin to a blockchain. However, what sets Arweave apart is its ability to accommodate transactions of varying sizes, a marked departure from traditional blockchains. This characteristic establishes a distinct and unparalleled attribute compared to other layer 1 networks. With Arweave, you acquire a combined cloud development and hosting platform. Arweave aligns with web standards, encompasses smart contracts, Profit Sharing Tokens (Token), Profit Sharing Communities (DAO), and NFTs (Non-Fungible Tokens). The beauty of the system is that you can harness these functionalities using a wide array of programming languages. If you're a full stack developer accustomed to technologies like javascript, graphql, and APIs, you possess the skills necessary to flourish within the Arweave ecosystem.
Create a free mapbox account to take advantage of the mapping and geocoding features. You will need a free developer public API key. https://www.mapbox.com
For the curious minded folks;
Fork the following repository: https://github.com/Ignatiusdork/arweaveHack/tree/main, then clone it to your local environment, or spin it up in gitpod: https://gitpod.io#[your forked repo]
Open a terminal window in the project directory and run:
yarn
Create a app/.env
, the .env
file will setup our environment settings for the application, we need to specify the following settings:
NOTE: make sure you create your
.env
file in theapp
directory.
app/.env
VITE_MAPBOX=MAPBOX_TOKEN_HERE
VITE_ARWEAVE_PORT=1984
VITE_ARWEAVE_PROTOCOL=http
VITE_ARWEAVE_HOST=localhost
These are a list of tools that we will be using from the Arweave ecosystem to help us with our application,
Arlocal allows us to run a devnet
local Arweave gateway/node, to iterate on our development locally. https://github.com/textury/arlocal
Arlocal is a great tool, I have not found any differences between my interaction with arlocal and the arweave.net, when my code works with arlocal, I am 100% confident it will work on arweave.net.
ArweaveJS is a javascript library that provides us with the APIs to post data on the weave and query data from the weave using GraphQL
. https://github.com/ArweaveTeam/arweave-js
Arweave js abstacts the JSON RPC api and wraps the commands using a javascript friendly promised based api, so you can generate wallets, create, sign, and post transactions.
ArweaveWalletConnector is a module that allows us to connect with the wallet application called arweave.app
. https://github.com/jfbeats/ArweaveWalletConnector
Arweave wallet connector interacts with the arweave.app wallet and gives you the connect, disconnect and several wallet connection events.
Arkb is a command-line application that publishes our web application to the permaweb. https://github.com/textury/arkb
arkb allows you to deploy your web application to the permaweb the layer on top of the arweave network that is managed by arweave gateways, the permaweb uses the tags on the arweave transaction to serve up specific transactions in the web browser. This technology allows you to deploy any web asset as a blockweave transaction.
Arlocal is a local server that implements the same API as the Arweave network, this enables you to develop locally then when ready deploy your app to https://arweave.net.
It is easy to setup, you simply install it from npm and setup a script for you to start the server.
npx arlocal
You can find out more about Arlocal in its repository - https://github.com/textury/arlocal - check out the readme, it has some cool options that you may be interested in, like running on a different port, or hiding the logs, or running programatically.
Open another terminal window and run:
yarn load-data
node publish-blog
This command is just setting up some transactional data for us to play with.
One of the most fascinating aspects of Arweave is that the gateway server seamlessly supports GraphQL right from the start! Yes, you heard it right. GraphQL can be used to query the blockweave right out of the box!
Ensure that your arlocal server is up and running. Then, navigate your browser to http://localhost:1984/graphql. A screen will pop up prompting you to launch the Apollo GraphQL visual tool. Click on this button, and you'll find yourself in the GraphQL query environment. Exciting, isn't it? ⭐️
This environment empowers you to craft GraphQL queries and execute them against your arlocal server. As an illustration, let's go ahead and run this query:
query {
transactions (tags: {
name: "App-Name"
values: ["PinPoint Earth"]
}) {
edges {
node {
id
tags {
name
value
}
data {
size
}
block {
id
}
}
}
}
}
You should see some results like:
{
"data": {
"transactions": {
"edges": [
{
"node": {
"id": "wOFui-eLxA8DKjgzUI9JDopa-EN9ARCxiG2oTcL0qjM",
"tags": [
{
"name": "App-Name",
"value": "PinPoint Earth"
},
{
"name": "Version",
"value": "0.1"
},
{
"name": "Content-Type",
"value": "image/png"
},
{
"name": "title",
"value": "Grand Central Terminal, NYC"
},
{
"name": "location",
"value": "33.074600,-79.762800"
},
{
"name": "timestamp",
"value": "2022-03-30T19:02:52.716Z"
}
],
"data": {
"size": "15720267"
},
"block": {
"id": "r7983ilr0sxvlwz2boo04luf9olhppqxee81dn9iqq67pnj7kugudi0iod89wv8w"
}
}
}
]
}
}
}
When we post our transaction with tags, we can use those tags as selectors in our Graphql
queries to filter our query requests. In our above example, we are using the tag name App-Name
and the tag value PinPoint Earth
as selector criteria in our Graphql query.
Given that Arweave operates as a decentralized framework for storing data, it offers an opportunity to transform into your own cloud backend. The mechanism involves crafting a blueprint for your data, much akin to a schema within a database. This blueprint defines the structure of your data, tailored to your application's needs. A pivotal component of an Arweave transaction is composed of various attributes, with the data and tags properties being particularly pertinent to our protocol design.
Arweave empowers the creation of tags for every transaction, albeit with a collective tag data capacity of 2048 bits. This assortment of tags can be strategically employed to facilitate query-friendly transactions using GraphQL. For our PinPoint Earth protocol, the objective centers on efficiently listing transactions on a map. Our GraphQL query mandates the provision of location data and titles. Moreover, we aspire to apply timestamp-based filtration to the listing. To this end, incorporation of timestamps into our tag specifications becomes imperative.
data: image
tags:
App-Name: 'PinPoint Earth' // constant
Protocol: 'PinPoint Earth' // constant
Content-Type: 'image/png|jpg|gif' // only support images
Title: 'Title of Pin' // limit 20 characters
Description: 'Description of Pin' // limit 100 characters
Location: 'lat, lng' // Latitude, Longitude
Timestamp: '' // new Date.toISOString()
With this simple protocol, anyone can drop a pin in any application, and will be able to find it and show it on a map.
Our primary aim is to establish a robust validation procedure that ensures the alignment of transactions originating from Arweave with our protocol. Equally important is the capability to generate transactions that can be both signed and uploaded onto the Arweave network. This particular schema model serves the purpose of validating our data in the context of PinPoint Earth.
We will use zod
to create a schema check:
import { z } from 'zod'
const schema = z.object({
id: z.string(),
title: z.string().max(20),
description: z.string().max(100),
location: z.string().max(50),
timestamp: z.string().max(50)
})
export const pinFromTx = (tx) => {
try {
return schema.parse({
id: tx.id,
title: (tx.tags.find(t => t.name === 'Title')).value,
description: (tx.tags.find(t => t.name === 'Description')).value,
location: tx.tags.find(t => t.name === 'Location').value,
timestamp: tx.tags.find(t => t.name === 'Timestamp').value
})
} catch (e) {
console.log(e)
return null
}
}
export const formToTx = (formData) => {
return {
data: formData.photo,
tags: [
{ name: 'App-Name', value: 'PinPoint Earth' },
{ name: 'Protocol', value: 'PinPoint Earth' },
{ name: 'Title', value: formData.title },
{ name: 'Location', value: formData.location },
{ name: 'Description', value: formData.description },
{ name: 'Timestamp', value: formData.timestamp }
]
}
}
This particular schema model serves the purpose of validating our data in the context of the blog for PinPoint Earth.
$: data = {
blogs: [],
loading: true,
};
$: blog_active_states = {};
async function loadData() {
const addr = "vVvN5qZ-KSno5hpsK_t2B-6gq2XGDENX3_ZE1HzzGs4";
var res = await getAllTxFromID(addr);
let _blogs = [];
await res.forEach(async (element) => {
let tx_details_res = await fetchTransaction(element.node.id);
const decoder = new TextDecoder("utf-8");
const decodedData = decoder.decode(tx_details_res.data);
blog_active_states[element.node.id] = false;
data.loading = false;
_blogs.push({
title: tx_details_res.tags[0].value,
body: decodedData,
id: element.node.id,
isActive: false,
});
console.log(_blogs);
});
data.blogs = _blogs;
}
loadData();
Shifting from web2 to web3 brings a unique challenge: transitioning from a model-based approach to a protocol-focused mindset. When making this shift, it's crucial to maintain a streamlined and straightforward protocol schema. This simplicity is key, as it allows for easy future expansions by creating new protocols and combining them as needed. Additionally, a vital step is consistently validating incoming data from the blockweave to ensure accuracy and integrity.
Arweave-js is the official javascript/typescript library for interacting with Arweave Blockweave.
cd app
yarn add arweave
Create a new file called arweave.js
in the app/src
folder
import Arweave from 'arweave'
// we want to use a `.env` file for arlocal and if not set, then use
// arweave.net for production
const arweave = Arweave.init({
host: import.meta.env.VITE_ARWEAVE_HOST || 'arweave.net',
port: import.meta.env.VITE_ARWEAVE_PORT || 443,
protocol: import.meta.env.VITE_ARWEAVE_PROTOCOL || 'https'
})
Import the arweave module and initialize our arweave constant using the
.env
variables or default to mainet. Mainet is the production version of Arweave's network.
Below the init block we will add our first arweave function:
export const activity = async () => {
try {
const result = await arweave.api.post('graphql', {
query: `
query {
transactions (first: 100, tags: { name: "Protocol", values: ["PinPoint Earth"] }) {
edges {
node {
id
tags {
name
value
}
}
}
}
}
`
})
return result?.data?.data?.transactions?.edges
} catch (e) {
return []
}
}
This function will query the Arweave network for all of the PinPoint Earth pins and return the most recent 100 pins dropped and all blog posts on the network if any using Graphql. You will notice the result returns two nested data objects, then transactions, and edges.
Now we are ready to create our first earth pin. Before we do that, lets mint some ar tokens for our local environment.
// https://[arlocal url]/mint/address/[winston 1000000000000000]
// Arweave.js
Lets build an Arweave transaction, we will use Arweave.js to build it.
export const submit = async ({ data, tags }) => {
// 1. Wallet not Connected!
if (window?.arweaveWallet === undefined) {
return { ok: false, message: 'Wallet not connected!' }
}
const balance = await getBalance(await window.arweaveWallet.getActiveAddress())
try {
const tx = await arweave.createTransaction({ data })
tags.map(({ name, value }) => tx.addTag(name, value))
await arweave.transactions.sign(tx)
// 2. check reward and wallet balance
if (Number(tx.reward) > Number(balance)) {
return { ok: false, message: 'Not Enough AR to complete request!' }
}
const uploader = await arweave.transactions.getUploader(tx)
return { ok: true, uploader, txId: tx.id }
} catch (e) {
return { ok: false, txId: tx.id, message: e.message }
}
}
let key;
const addr = await arweave.wallets.jwkToAddress(key)
const tokens = arweave.ar.arToWinston(1)
// mint some tokens
await arweave.api.get(`mint/${addr}/${tokens}`)
await arweave.api.get('mine')
tx = await arweave.createTransaction({
data: data3, tags: [new Tag('blog_title', 'Arweavely Hack')]
}, key)
await arweave.transactions.sign(tx, key)
console.log(tx.id)
uploader = await arweave.transactions.getUploader(tx)
while (!uploader.isComplete) {
await uploader.uploadChunk()
}
await arweave.api.get('mine')
This function will take care of the create, sign and upload of our arweave transaction, and using our pin model we can validate the form entry data and convert to a transactionObject
this is just a simple object that can be sent to the the submitTx function.
Next, we need to add a waitfor function, this function will gracefully check the Arweave gateway for a status on the processing of a transaction. Once the transaction is uploaded, we need verification from the gateway that the transaction has been accepted.
export const waitfor = async (txId) => {
let count = 0;
let foundPost = null;
while (!foundPost) {
count += 1;
console.log(`attempt ${count}`);
await delay(2000 * count);
const result = await arweave.api.post('graphql', {
query: `
query {
transaction(id: "${txId}") {
id
}
}
`});
foundPost = result.data.data.transaction.id === txId;
if (count > 10) {
break; // could not find post
}
}
return foundPost
}
In order to view our newly saved transaction, we need to implement the getTx function in the src/arweave.js
file.
src/arweave.js
export const getTx = async (id) => {
try {
const result = await arweave.api.post('graphql', {
query: `
query {
transaction(id: "${id}") {
id
owner {
address
}
tags {
name
value
}
}
}
`
})
return result?.data?.data?.transaction
} catch (e) {
return null
}
}
const getAllTxFromID = async (id) => {
try {
const result = await arweave.api.post('graphql', {
query: `
query {
transactions(owners: ["${id}"]) {
edges {
node {
id
}
}
}
}
`
})
return result?.data?.data?.transaction
} catch (e) {
return null
}
}
Yeeh! We have reached the end of explaning the core concepts of the PinPoint Earth dApp. in this breakdown we covered:
There's a wealth of untapped potential within Arweave, and this marks only the initial phase of our journey into exploration!