
You can now comment on my Mirror entries! Here’s how I implemented it in a decentralized way.
For a while now I’ve been maintaining a custom Mirror client. This allows me to use a custom domain (m1guelpf.blog instead of miguel.mirror.xyz), provide an RSS feed and retain some control over the design.
So, when I came across The Convo Space, a “conversation protocol” built on top of IPFS & Libp2p, I decided to try my hand at building a decentralized commenting system for the client.
To get started, we need a way of getting the comments for the current entry. TheConvo works with threads and, fortunately, we can make up our own thread IDs. Since Mirror digests are supposed to be unique, we can use those as our thread ID, linking the comments to the entry regardless of the URL it’s displayed at.
With our thread ID figured out, we can use Vercel’s SWR library to fetch comments on page load.
const { data: comments, mutate } = useSWR(
`https://theconvo.space/api/comments?apikey=CONVO&threadId=${entry.digest}`,
{ revalidateOnFocus: false }
)
This gets us an array of comments we can loop over, including the comment’s contents, timestamp & address/ENS of the author. Since my design also included an avatar field, I tweaked the fetcher function to look at the avatar field of the commenters’ ENS domains, defaulting to a gradient avatar if they hadn’t set one or didn’t have an ENS domain.
const DEFAULT_AVATAR = 'https://cdn.tryshowtime.com/profile_placeholder.jpg'
const getAvatarFromENS = ensName => {
if (! ensName) return DEFAULT_AVATAR
return serverWeb3
.getResolver(ensName)
.then(resolver => resolver?.getText('avatar') || DEFAULT_AVATAR)
}
const commentFetcher = url =>
fetch(url)
.then(res => res.json())
.then(comments => Promise.all(comments.map(async comment => ({
...comment,
authorAvatar: getAvatarFromENS(comment.authorENS),
}))))
const { data: comments, mutate } = useSWR(
`https://theconvo.space/api/comments?apikey=CONVO&threadId=${digest}`,
commentFetcher,
{ revalidateOnFocus: false }
)
With a system to read comments in place, we now need a way to post them. TheConvo’s process for this is pretty straightforward: authenticate the user using a personal signature and send an API request to post the comment.
To authenticate the users, we need to craft a special signature composed of the user’s wallet address and the current timestamp.
const timestamp = Date.now()
const signerAddress = await web3.getSigner().getAddress()
const signature = await web3.getSigner().signMessage(
`I allow this site to access my data on The Convo Space using the account ${signerAddress}. Timestamp:${timestamp}`
)
const token = await axios.post(
'https://theconvo.space/api/auth?apikey=CONVO',
{ signerAddress, signature, timestamp }
).then(res => res.data?.message)
This token will be valid for 1 day, so I wanted to avoid prompting the user for a signature more than once a day. I thought about storing the token on localStorage along with an expiry date but ended up going with a cookie, as you can set those to expire automatically. Instead of calling the Convo API directly, I created an API route to handle login.
import axios from 'axios'
import { serialize } from 'cookie'
const ONE_DAY = 60 * 60 * 24 * 1000
function createCookie(name, data, options = {}) {
return serialize(name, data, {
maxAge: ONE_DAY,
expires: new Date(Date.now() + ONE_DAY * 1000),
secure: process.env.NODE_ENV === 'production',
path: '/',
httpOnly: true,
sameSite: 'lax',
...options,
})
}
export default async ({ method, body: { signerAddress, signature, timestamp } }, res) => {
if (method != 'POST') return res.status(405).send('Not Found')
const token = await axios.post(
'https://theconvo.space/api/auth?apikey=CONVO',
{ signerAddress, signature, timestamp }
).then(res => res.data?.message)
res.setHeader('Set-Cookie', [
createCookie('convo_token', token),
createCookie('convo_authed', true, { httpOnly: false })
])
res.status(200).end()
}
Notice how I’m setting two cookies. The first one, convo_token, contains the actual token but is not accessible from JS. A second one, convo_authed, allows us to check if the token exists from the frontend, without exposing it. Here’s our updated code for the front end.
const authenticateConvo = async web3 => {
const timestamp = Date.now()
const signerAddress = await web3.getSigner().getAddress()
const signature = await web3.getSigner().signMessage(
`I allow this site to access my data on The Convo Space using the account ${signerAddress}. Timestamp:${timestamp}`
)
await axios.post('/api/comments/login', {
signerAddress, signature, timestamp
})
}
if (!document.cookie.includes('convo_authed')) await authenticateConvo(web3)
Finally, posting the comment is as simple as making a POST request to the /api/comments endpoint. Since the frontend doesn’t have access to the Convo token, I created another API route for this.
import axios from 'axios'
export default async ({ method, headers: { referer }, body: { signerAddress, comment, digest }, cookies: { convo_token } }, res) => {
if (method != 'POST') return res.status(405).send('Not Found')
if (!comment || !digest || !signerAddress) return res.status(400).send('Invalid Params')
if (!convo_token) return res.status(401).send('Unauthorized')
try {
await axios.post(
'https://theconvo.space/api/validateAuth?apikey=CONVO',
{ token: convo_token, signerAddress }
)
} catch {
return res.status(401).send('Unauthorized')
}
await axios.post(
'https://theconvo.space/api/comments?apikey=CONVO',
{ token: convo_token, signerAddress, comment, threadId: digest, url: referer }
).then(resp => res.json(resp.data))
}
Back on the front end, we can use SWR’s mutate function to instantly display the comment at the end of our list (while we retry the request to fetch the new data).
const result = await axios.post('/api/comments/post', {
signerAddress: await web3.getSigner().getAddress(), comment, digest
}).then(res => res.data)
setComment('')
mutate(comments => {
comments.push({
...result,
authorAvatar: 'https://cdn.tryshowtime.com/profile_placeholder.jpg'
})
}, true)
Hope you enjoyed the post! The Mirror client I mentioned at the start is open-source, and you can look at the commit that added the comment system here.
You can follow me on Twitter to keep up with what I’m building, or leave a comment below to try the whole thing out!
Gas Optimizations for the Rest of Us
The basics of optimizing Solidity contracts, explained for regular coders.Writing smart contracts is hard. Not only do you get a single chance to write bug-free code, but depending on exactly how you write, it’ll cost your users more or less to interact with it. When you compile a smart contract, every line of Solidity gets converted into a series of operations (called opcodes), which have a set gas cost. Your goal is to write your program using as little opcodes as possible (or replace the m...
How to Learn (Crypto)
Outlining my approach to quickly learning new subjects, and how you too can become a crypto expert in less than you think.In the past few months, I went from knowing almost nothing about crypto to minting NFTs, launching my own token, and getting a job at a crypto startup (TBA). While part of this was definitely being in the right place at the right time, the rest is most likely thanks to a learning strategy I’ve developed over the years, and that helped me structure my dive into the crypto w...
Web3 is not a space, but a tool
Brain dump on post-crypto perspective, and re-exploring who I want to be.I learnt to code because I wanted to "build things". For years, I built app after app, not only as a way to learn more and more, but also to bring my ideas to life. I learned how to do frontend, backend, design, even how to do marketing and to PM myself, in a quest to achieve all the skills necessary to create everything I wanted to. To learn enough to be dangerous. Getting into crypto derailed this a little bit, and I f...
<100 subscribers

You can now comment on my Mirror entries! Here’s how I implemented it in a decentralized way.
For a while now I’ve been maintaining a custom Mirror client. This allows me to use a custom domain (m1guelpf.blog instead of miguel.mirror.xyz), provide an RSS feed and retain some control over the design.
So, when I came across The Convo Space, a “conversation protocol” built on top of IPFS & Libp2p, I decided to try my hand at building a decentralized commenting system for the client.
To get started, we need a way of getting the comments for the current entry. TheConvo works with threads and, fortunately, we can make up our own thread IDs. Since Mirror digests are supposed to be unique, we can use those as our thread ID, linking the comments to the entry regardless of the URL it’s displayed at.
With our thread ID figured out, we can use Vercel’s SWR library to fetch comments on page load.
const { data: comments, mutate } = useSWR(
`https://theconvo.space/api/comments?apikey=CONVO&threadId=${entry.digest}`,
{ revalidateOnFocus: false }
)
This gets us an array of comments we can loop over, including the comment’s contents, timestamp & address/ENS of the author. Since my design also included an avatar field, I tweaked the fetcher function to look at the avatar field of the commenters’ ENS domains, defaulting to a gradient avatar if they hadn’t set one or didn’t have an ENS domain.
const DEFAULT_AVATAR = 'https://cdn.tryshowtime.com/profile_placeholder.jpg'
const getAvatarFromENS = ensName => {
if (! ensName) return DEFAULT_AVATAR
return serverWeb3
.getResolver(ensName)
.then(resolver => resolver?.getText('avatar') || DEFAULT_AVATAR)
}
const commentFetcher = url =>
fetch(url)
.then(res => res.json())
.then(comments => Promise.all(comments.map(async comment => ({
...comment,
authorAvatar: getAvatarFromENS(comment.authorENS),
}))))
const { data: comments, mutate } = useSWR(
`https://theconvo.space/api/comments?apikey=CONVO&threadId=${digest}`,
commentFetcher,
{ revalidateOnFocus: false }
)
With a system to read comments in place, we now need a way to post them. TheConvo’s process for this is pretty straightforward: authenticate the user using a personal signature and send an API request to post the comment.
To authenticate the users, we need to craft a special signature composed of the user’s wallet address and the current timestamp.
const timestamp = Date.now()
const signerAddress = await web3.getSigner().getAddress()
const signature = await web3.getSigner().signMessage(
`I allow this site to access my data on The Convo Space using the account ${signerAddress}. Timestamp:${timestamp}`
)
const token = await axios.post(
'https://theconvo.space/api/auth?apikey=CONVO',
{ signerAddress, signature, timestamp }
).then(res => res.data?.message)
This token will be valid for 1 day, so I wanted to avoid prompting the user for a signature more than once a day. I thought about storing the token on localStorage along with an expiry date but ended up going with a cookie, as you can set those to expire automatically. Instead of calling the Convo API directly, I created an API route to handle login.
import axios from 'axios'
import { serialize } from 'cookie'
const ONE_DAY = 60 * 60 * 24 * 1000
function createCookie(name, data, options = {}) {
return serialize(name, data, {
maxAge: ONE_DAY,
expires: new Date(Date.now() + ONE_DAY * 1000),
secure: process.env.NODE_ENV === 'production',
path: '/',
httpOnly: true,
sameSite: 'lax',
...options,
})
}
export default async ({ method, body: { signerAddress, signature, timestamp } }, res) => {
if (method != 'POST') return res.status(405).send('Not Found')
const token = await axios.post(
'https://theconvo.space/api/auth?apikey=CONVO',
{ signerAddress, signature, timestamp }
).then(res => res.data?.message)
res.setHeader('Set-Cookie', [
createCookie('convo_token', token),
createCookie('convo_authed', true, { httpOnly: false })
])
res.status(200).end()
}
Notice how I’m setting two cookies. The first one, convo_token, contains the actual token but is not accessible from JS. A second one, convo_authed, allows us to check if the token exists from the frontend, without exposing it. Here’s our updated code for the front end.
const authenticateConvo = async web3 => {
const timestamp = Date.now()
const signerAddress = await web3.getSigner().getAddress()
const signature = await web3.getSigner().signMessage(
`I allow this site to access my data on The Convo Space using the account ${signerAddress}. Timestamp:${timestamp}`
)
await axios.post('/api/comments/login', {
signerAddress, signature, timestamp
})
}
if (!document.cookie.includes('convo_authed')) await authenticateConvo(web3)
Finally, posting the comment is as simple as making a POST request to the /api/comments endpoint. Since the frontend doesn’t have access to the Convo token, I created another API route for this.
import axios from 'axios'
export default async ({ method, headers: { referer }, body: { signerAddress, comment, digest }, cookies: { convo_token } }, res) => {
if (method != 'POST') return res.status(405).send('Not Found')
if (!comment || !digest || !signerAddress) return res.status(400).send('Invalid Params')
if (!convo_token) return res.status(401).send('Unauthorized')
try {
await axios.post(
'https://theconvo.space/api/validateAuth?apikey=CONVO',
{ token: convo_token, signerAddress }
)
} catch {
return res.status(401).send('Unauthorized')
}
await axios.post(
'https://theconvo.space/api/comments?apikey=CONVO',
{ token: convo_token, signerAddress, comment, threadId: digest, url: referer }
).then(resp => res.json(resp.data))
}
Back on the front end, we can use SWR’s mutate function to instantly display the comment at the end of our list (while we retry the request to fetch the new data).
const result = await axios.post('/api/comments/post', {
signerAddress: await web3.getSigner().getAddress(), comment, digest
}).then(res => res.data)
setComment('')
mutate(comments => {
comments.push({
...result,
authorAvatar: 'https://cdn.tryshowtime.com/profile_placeholder.jpg'
})
}, true)
Hope you enjoyed the post! The Mirror client I mentioned at the start is open-source, and you can look at the commit that added the comment system here.
You can follow me on Twitter to keep up with what I’m building, or leave a comment below to try the whole thing out!
Gas Optimizations for the Rest of Us
The basics of optimizing Solidity contracts, explained for regular coders.Writing smart contracts is hard. Not only do you get a single chance to write bug-free code, but depending on exactly how you write, it’ll cost your users more or less to interact with it. When you compile a smart contract, every line of Solidity gets converted into a series of operations (called opcodes), which have a set gas cost. Your goal is to write your program using as little opcodes as possible (or replace the m...
How to Learn (Crypto)
Outlining my approach to quickly learning new subjects, and how you too can become a crypto expert in less than you think.In the past few months, I went from knowing almost nothing about crypto to minting NFTs, launching my own token, and getting a job at a crypto startup (TBA). While part of this was definitely being in the right place at the right time, the rest is most likely thanks to a learning strategy I’ve developed over the years, and that helped me structure my dive into the crypto w...
Web3 is not a space, but a tool
Brain dump on post-crypto perspective, and re-exploring who I want to be.I learnt to code because I wanted to "build things". For years, I built app after app, not only as a way to learn more and more, but also to bring my ideas to life. I learned how to do frontend, backend, design, even how to do marketing and to PM myself, in a quest to achieve all the skills necessary to create everything I wanted to. To learn enough to be dangerous. Getting into crypto derailed this a little bit, and I f...
Share Dialog
Share Dialog
1 comment
Building on the concept popularized by Wordle, Dordle doubles the challenge without complicating the rules. https://dordle.io