Building a Project with SE-2 | Crowd Fund | Part Nine | Followers and Posts

WebSculpt
5 min readMar 21, 2024

--

Image from Shubham Dhage on Unsplash

If you are here for the first time, then you may want to start at the beginning of this series.
You are currently on the first article for Crowd Fund V4 — if you wish to go back through this series, you can view all of my blogs here.
Here are some links to the project files that are helpful to have open while going through this blog:

The next upgrade that our Crowd Fund dApp will obtain is going to allow us to use it more like a Social Media App — with posts, likes, and comments.

We always want to stick to decentralization, so — in order to make a post — every member of a Fund Run must agree on (support) the post (before the Fund Run’s followers will see it). This adds governance to posts, so this should prevent any negligent or malicious misuse of this feature.

In this dApp, we will only allow Posts from Fund Runs, but everyone can comment on them.

Let’s dive in

Posts (and their proposals) will be VERY similar to the way in which Transfer Proposals were built in the previous version. Members of a Fund Run will manage potential Posts via a screen that looks a lot like the Transfer Vault — You create a “Proposal” (to create a Post) like this:

To create a new proposal for a Social Media Post, you will create a signature (just like before) — then send that signature along with the SocialMediaRequest to the createSocialProposal function in SocialPostManager.sol.

/**
* @dev new structs in V4
* Multisig Request to make
* a social media post
*/
struct SocialMediaRequest {
string postText;
address proposedBy;
}

☝️ The new struct for a Post proposal — in CrowdFundLibrary.sol

👇 The createSocialProposal function — in SocialPostManager.sol

function createSocialProposal(
bytes calldata _signature,
uint16 _id,
CrowdFundLibrary.SocialMediaRequest calldata _tx
) external ownsThisFundRun(_id, msg.sender, true) {
SocialProposalStatus thisStatus = SocialProposalStatus(0);
socialProposalCreators[numberOfSocialProposals] = msg.sender;
socialProposalStatuses[numberOfSocialProposals] = thisStatus;
socialProposalSigners[numberOfSocialProposals].push(msg.sender);

emit SocialProposal(
numberOfSocialProposals,
_id,
msg.sender,
_tx.postText,
thisStatus,
fundRunOwners[_id].length,
0
);

emit SocialProposalSignature(
numberOfSocialProposals,
msg.sender,
_signature
);

numberOfSocialProposals++;
}

☝️ In the code above, we have some mappings (that we will use in our modifiers to prevent misuse) along with two events. Note that I am only using the numberOfSocialProposals in order to sort them in our query-results later on.

👇 Below is the code to finalize and post (this Post) to your followers…

function finalizeAndPost(
CrowdFundLibrary.SocialMediaRequest calldata _tx,
uint256 _nonce,
uint16 _id,
uint16 _socialProposalId,
bytes[] calldata _signaturesList
)
external
nonReentrant
ownsThisFundRun(_id, msg.sender, true)
postNotSent(_socialProposalId)
socialProposalNotRevoked(_socialProposalId)
{
_verifySocialRequest(_tx, _nonce, _signaturesList, _id);
socialProposalStatuses[_socialProposalId] = SocialProposalStatus(2);
emit SocialPost(_socialProposalId, _id, _tx.proposedBy, _tx.postText);
}

Once again, this is very similar to the previous version’s Transfer Proposals — please see this blog if you have not already.

Interacting with Posts

Now that we have a new Post out there — we want people to be able to “Like” it, follow our account, and hop into the conversation with us in the comments!

Followers

I am happy that we are getting into the magic of subgraphs this early in the blog. The code to Follow or Unfollow a Fund Run is in FollowersManager.sol 👇

contract FollowersManager {
event Follow(uint16 fundRunId, address user);

event Unfollow(uint16 fundRunId, address user);

function follow(uint16 _fundRunId) external {
emit Follow(_fundRunId, msg.sender);
}

function unfollow(uint16 _fundRunId) external {
emit Unfollow(_fundRunId, msg.sender);
}
}

That’s it! When The Graph indexes this data, we can query it like this 👇

return gql`
query ($fundRunId: Int!, $user: String!) {
fundRuns(where: { fundRunId: $fundRunId }) {
followers(where: { user: $user }) {
id
fundRunId
user
}
}
}
`;

The query to retrieve all of the Fund Runs that a user is following 👇

  return gql`
query ($limit: Int!, $offset: Int!, $user: String!) {
follows(
orderBy: fundRunId
orderDirection: desc
first: $limit
skip: $offset
where: { user: $user, fundRun_not: null }
) {
id
fundRun {
id
fundRunId
title
description
amountCollected
amountWithdrawn
}
}
}
`;

The query to retrieve all of the users that follow a particular Fund Run 👇

  return gql`
query ($limit: Int!, $offset: Int!, $fundRunId: Int!) {
follows(first: $limit, skip: $offset, where: { fundRunId: $fundRunId, fundRun_not: null }) {
id
user
}
}
`;

Liking a Post

We are going to utilize The Graph here as well — we will simply emit an event and query its details later👇

event PostLike (bytes postId, address userWhoLiked);

function likePost(bytes memory _postId) external {
emit PostLike(_postId, msg.sender);
}

In our schema.graphql, you can see that (even though we aren’t keeping up with them in the contract) we have a way to retrieve the number of times a post has been liked:

type SocialPost @entity {
id: Bytes!
socialProposalId: Int!
fundRunId: Int!
fundRunTitle: String!
proposedBy: Bytes!
postText: String!
blockNumber: BigInt!
blockTimestamp: BigInt!
transactionHash: Bytes!
fundRun: FundRun
comments: [Comment!] @derivedFrom(field: "socialPost")
likes: [PostLike!] @derivedFrom(field: "post")
likeCount: Int! # <<<<< Like Count here
}

type PostLike @entity {
id: Bytes!
postId: Bytes!
userWhoLiked: Bytes!
blockNumber: BigInt!
blockTimestamp: BigInt!
transactionHash: Bytes!
post: SocialPost # How a Like relates to its Post
}

But … if our contract doesn’t know about this likeCount, then how will our frontend?

That is where mapping.ts comes in:

export function handlePostLike(event: PostLikeEvent): void {
let entity = new PostLike(
event.transaction.hash.concatI32(event.logIndex.toI32())
);
entity.postId = event.params.postId;

.......

//load the Post Entity and
let postEntity = SocialPost.load(event.params.postId);
if (postEntity !== null) {
entity.post = postEntity.id;
postEntity.likeCount++; //likeCount incremented
postEntity.save();
}

entity.save();
}

☝️ When a Post gets a new Like, the Post Entity is loaded and the likeCount is incremented. This is how we can have access to this data without needing to store ALL of these details in our contract.

A look at posts and their likes

Adding a Tip to a Post

I know that you just saw that little TIP button on these Posts. Since each Post has the details of its Fund Run, we can simply call an old function that we’ve been using for a while now: donateToFundRun

I set Tips to 0.001 Ether, but feel free to change it.

Comments

In the next blog, I will cover how to make comments and sub-comments using a Smart Contract and a subgraph 👇

--

--

WebSculpt

Blockchain Development, coding on Ethereum. Condensed notes for learning to code in Solidity faster.