How it Works: Friendcaster

Sep 9, 2022

I was just scrolling on Farcaster and this cast scrolled onto my feed:

The cast that gave me the idea to build Friendcaster.

The cast that gave me the idea to build Friendcaster.

I clicked on the link to see what Chirpty was, and felt that this would be a perfect project for me. It’s simple, but not too simple.

How it Works

All the code is available on Github

This is the root dir of my project. I use typescript so I had to make some changes to package.json and tsconfig.json but the basic initialization was done via npm init -y.

├── index.ts
├── src
│   ├── api.ts
│   ├── data.ts
│   ├── final.ts
│   └── image.ts
└── views
    ├── pages
    │   ├── circle.ejs
    │   ├── faq.ejs
    │   └── index.ejs
    └── partials
        ├── footer.ejs
        ├── head.ejs
        └── header.ejs
├── public
│   ├── circle.png
│   └── favicon.ico
├── .env
├── package.json
├── tsconfig.json

The code is split into multiple files for better maintainability:

Fetching Data

src/api.ts

We’ll need to understand a bit about how Farcaster stores user data, to understand the calls we make. Farcaster allows users to store their casts (tweets) on whatever server they wish to. The server could be AWS s3, a VPS on Digital Ocean or a Raspberry Pi under their desk. This keeps the cost of posting low (cuz storing data on the blockchain is high). But to incentivize the servers to not misbehave, users are able to switch the server that stores the data. The user just need copy their data to the new server and change the url in the Farcaster Name Registry - a smart contract that maps an address -> username -> url of the host directory.

So to fetch a user’s posts we need to query the smart contract, with the username, to get the url of the server that stores their data, then query the data from that url.

I’m using ethers.js to interact with the blockchain, and Alchemy as a node service.

To get the url of a user’s host directory, we call the getDirectoryUrl function defined in the Name Registry Contract with the username formatted in bytes32 string. Then we get the data stored in the host directory using got() .

export const getDirectory = async (username: string, registryContract:any): Promise<IDirectory>  => {

  const byte32Name = utils.formatBytes32String(username);
  const directoryUrl = await registryContract.getDirectoryUrl(byte32Name);

  const directoryResponse = await got(directoryUrl, { protocol: 'https:' });
  const directoryBody = JSON.parse(directoryResponse.body).body;

  return {
    directoryUrl,
    directoryBody
  };
}

The data stored in the host directory is of the shape:

{
  body: {
    addressActivityUrl: 'http://www.host.xyz/bob/casts.json',
    avatarUrl: 'https://github.com/bob_the_builder.png',
    displayName: 'Bob',
    proofUrl: 'http://www.host.xyz/bob/proof.json',
    timestamp: 1624314341272,
    version: 1,
  },
  merkleRoot: string,
  signature: string,
}

I’ve defined two more functions in this file is getAvatarUrl, getAvatarListUrl. They fetch the avatar url of a user and a list of users, respectively. They call getDirectory extract the url from the directory blob.

const { directoryBody } = await getDirectory(username, registryContract);
const avatarUrl = directoryBody.avatarUrl;

Processing that data to look for posts that are replies

src/data.ts

Farcaster stores all of user’s posts in the directory as a json file, that is referenced by the directoryBody.addressActivityUrl. The json file is shaped like this:

interface IAction {

  body: Object;
  merkleRoot: string;
  signature: string;
  meta: {
    displayName: string;
    avatar: string;
    isVerifiedAvatar: boolean;
    numReplyChildren: number;
    reactions: Object;
    recasts: Object;
    watches: Object;
    replyParentUsername?: {
      address: string;
      username: string;
    }
  }

}

Reply posts have the replyParentUsername key defined while normal posts don’t have this key cuz they are the parent. To filter posts that are replies vs the posts that are just normal posts we define this filter on an array containing all the user’s timeline. We then map over the filtered array to fetch the username of the post our user replied to.

const addressActivity: IAction[] = JSON.parse(addressActivityResponse.body);
const repliedPeople: string[] = addressActivity.filter((action:IAction) => {
  // check if the action is a associated with another user.
  if (!action.meta.replyParentUsername) {
    return false;
  }

  return true;
}).map((action:any) => action.meta.replyParentUsername.username)

Now we have the username of everyone that we replied to. The final circle contains several rings, which represent our frequency of interaction. We need to add weights to the array we have.

const interactionFrequency: IFreqObj = interactedPeople.reduce((prevValue: any, currentValue: string) => {
  return prevValue[currentValue] ? ++prevValue[currentValue] : prevValue[currentValue] = 1, prevValue
}, {});

const tally: INameFreq[] | any[] = []
for (const [uname, freq] of Object.entries(interactionFrequency)) {
  tally.push({
      username: uname,
      freq: freq
  });
}

tally.sort((a:any, b:any) => b.freq - a.freq);

We add weight to our array of usernames using the reduce function. We then sort the array in the tally array.

Next we add the caller’s username to the list, and the append portions of the array into a result array. This is mostly to handle the data for different rings separately.

head = [{username: username, freq: 0, avatarUrl: userUrl}, ...head];

let result: INameFreq[][] = []
result.push(head.splice(0, 1))
result.push(head.splice(0, 8))
result.push(head.splice(0, 15))
result.push(head.splice(0, 26))
return result;

Generating the Image

src/image.ts

I basically copied the image generation style from Chirpty which uses node-canvas . MY render function accepts a config object of interface IConfig and writes the generated image to public/circle.png. Since I’m running Express.js I can set that image as my static directory and access the image from anywhere.

interface IConfig {
  users: INameFreq[];
  count: number;
  radius: number;
  distance: number;
}

Tying everything into a single endpoint for the server:

src/final.ts

This file contains the contract address and the contract ABI which are required to generate a contract instance. This file instantiates the contract and calls the getInteractionFrequency with it. then supplies the data to the render function.

export async function createImage(username: string) {
  const provider = new providers.AlchemyProvider('rinkeby', process.env.ALCHEMY_API_KEY);
  const registryContract = new Contract(REGISTRY_CONTRACT_ADDRESS, REGISTRY_ABI, provider);

  const block = await provider.getBlockNumber();
  console.log("Block: ", block);

  const data = await getInteractionFrequency(username, registryContract)
  await render([
    {distance: 0,   count: 1,   radius: 110,    users: data[0]},
	  {distance: 300, count: 8,   radius: 80,     users: data[1]},
	  {distance: 400, count: 15,  radius: 50,     users: data[2]},
	  {distance: 500, count: 26,  radius: 30,     users: data[3]},
  ]);
};

Setting up Express.js in

index.ts

It contains generic code to setup some basic routes, and public directories. Only one route is of relevance here. It is the route that returns the interaction circle image.

app.get("/circle/:username", (req, res, next) => {
  let username = req.params.username;
  if (username[0] == '@') username = username.slice(1,);

  createImage(username)
    .then(e => {
      setTimeout(() => (res.render('pages/circle'), 1))
    }).catch(next)
});

When the user enters their username in the form (on the homepage), they are sent to the /circle/<username> route. This will trigger the function createImage, defined in src/final.ts. After the image has been written to public/circle.png file, we tell Express.js to render the page using /views/pages/circle view. The view contains an image tag which renders the interaction circle.

Conclusion

Friendcaster was the first project that people other than my irl family used. It was a real product. That helped people. And the response from the fc community was awesome. I got some really good feedback and feature ideas, and I feel that it would be a good idea to launch a version 2 of friendcaster.


How Jack Butcher Productized Himself

Blockchains Scale Socially

Have a comment or response? Email me.