Building apps with Mirror

Over the past few weeks, I built a few apps that integrated with Mirror, to try and understand how their protocol worked. I started using their internal APIs and, from there, worked my way to more decentralized sources. Here’s how I did it (and how you can too).

The Actual Write Race

The idea for The Actual Write Race was to build a list of all existing Mirror publications, and rank them based on the number of articles they had written (simulating Mirror’s $WRITE race) to add a fun touch.

To get the list of publications, we can query the Mirror GraphQL API (live at https://mirror-api.com/graphql) with the following query:

query FetchPublications {
	publications {
		ensLabel
		displayName
		avatarURL
		contributor {
			displayName
			avatarURL
			address
		}
	}
}

The publications query will give us most of the data we need, and if I were to build a simple listing I could have stopped here, but I also need the number of entries to rank them. Ideally, I should just be able to fetch the entries key on the above query, but due to how Mirror has structured their API, the entries are set to null when querying the publication list. Instead, we can use a second query to fetch the entries for each publication and check the length of those.

query PublicationEntries($ensLabel: String!) {
	publication(ensLabel: $ensLabel) {
		ensLabel
		entries {
			digest
		}
	}
}

It’s not the best system, and it has a blatant N+1 issue, but it’s the best I could get for this project, and the data is only fetched once a day, so it didn’t end up being an issue. If you want to learn more about this project, the source is available on GitHub.

Mirror Client

The Mirror interface is beautiful, and I wanted to take a chance at recreating it with Tailwind CSS, so I decided to build a custom Mirror client with Next.js. The hard part of this project turned out to be retrieving Mirror entries in a decentralized way (querying the blockweave instead of Mirror’s API).

To start, we need to know the wallet address of the publication owner. Since Mirror subdomains are ENS names, we can do this by resolving {publication}.mirror.xyz with any ENS resolver. With this information, we can query the blockweave (which conveniently offers a GraphQL API hosted at https://arweave.net/graphql) by retrieving transactions created by Mirror and signed by that wallet address:

query FetchTransactions($address: String!) {
	transactions(first: 100, tags: [{ name: "App-Name", values: ["MirrorXYZ"] }, { name: "Contributor", values: [$address] }]) {
		edges {
			node {
				id
				tags {
					name
					value
				}
			}
		}
	}
}

Since Mirror supports editing entries by pushing additional transactions, we need to check the Original-Content-Digest tag to make sure we only take the latest edition of each entry into account. We’ll use that original digest as the slug for the post (emulating Mirror) and the node ID to fetch the entry from the blockweave using the Arweave NPM library.

const getPaths = async () => {
	const {
		data: {
			transactions: { edges },
		},
	} = await queryGraphQL()

	edges.map(({ node }) => {
		const tags = Object.fromEntries(node.tags.map(tag => [tag.name, tag.value]))
		
		return { slug: tags['Original-Content-Digest'], path: node.id }
	}).filter(entry => entry.slug && entry.slug !== '').reduce((acc, current) => {
		const x = acc.find(entry => entry.slug === current.slug)
		
		if (!x) return acc.concat([current])
		else return acc
	}, [])
}

const getEntries = async () => {
	const paths = await getPaths()
		
	return Promise.all(
		paths.map(async entry => JSON.parse(
			await arweave.transactions.getData(entry.path, { decode: true, string: true }), entry.slug)
		)
	)
}

This will get you an array of entries following this format, you can then just parse the markdown bodies and render your entries.

Then, to fetch the contents of a single entry, you can query by the original content digest (which we’re using as a slug).

query FetchTransaction($digest: String!) {
	transactions(tags: { name: "Original-Content-Digest", values: [$digest] }) {
		edges {
			node {
				id
			}
		}
	}
}

Keep in mind in this example we’re not verifying the signature of any of these entries, so anyone could add new entries with a random string as the signature. Ideally, you’d use a library like eth-sig-util to make sure all entries are authentic.

If you’re curious about the source of my Mirror client, it’s available on GitHub. You can also see it live at m1guelpf.blog.

Extending Mirror

With these two data sources (Mirror’s GraphQL API & the blockweave), you can build anything on top of Mirror. Here are a few ideas:

  • RSS feeds for Mirror blogs (my Mirror client already includes an RSS feed, you’d just need to make it work with any other publication.
  • A list of all the available crowdfunds throughout Mirror publications, as well as some stats on finished ones.
  • A substack-like app that watches for new entries on a specific publication (or maybe all of them?) and emails subscribers the content.
  • A interface allowing people who don’t yet have a Mirror publication to publish their articles on Arweave using the same format Mirror uses.

Make sure to send me any cool apps you build with Mirror! You can find me at @m1guelpf on Twitter.

Subscribe to Miguel Piedrafita
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.