diff --git a/web3_social_media/README.md b/web3_social_media/README.md new file mode 100644 index 00000000..b0a96d5b --- /dev/null +++ b/web3_social_media/README.md @@ -0,0 +1,97 @@ +# Social Media Smart Contract +- Note: This project include an [article](https://medium.com/@aycrown77/resolving-a-getcontractfactory-error-in-hardhat-a-step-by-step-guide-d901fc186815) on how I was able to resolve the deployment error I was facing. +## Overview + +This Ethereum smart contract functions as a basic social media platform, allowing users to create posts, like posts created by others, and view their own and other users' posts. + +## Prerequisites + +- An Ethereum wallet and access to a blockchain network (i.e Buildbear Etherium testnet) with sufficient ETH for transaction fees. +- An Ethereum development environment, such as Remix or a development framework like Truffle. +- Solidity compiler version 0.8.13 or compatible. + +## Contract Details + +- **Solidity Version**: ^0.8.13 +- **Owner**: The owner of this contract is set at deployment and is the Ethereum address that deploys it. The owner has special privileges, such as changing the maximum post length. + +## Features + +### 1. Create Post + +- **Function**: `createPost(string memory _post)` +- Allows a user to create a new post. +- Verifies that the post length does not exceed the maximum allowed length (`MAX_POST_LENGHT`). +- Emits a `postCreated` event upon successful post creation. + +### 2. Like Post + +- **Function**: `likeTweet(address author, uint256 id) external` +- Allows a user to like a post created by another user. +- Emits a `postLiked` event upon successful liking. + +### 3. Unlike Post + +- **Function**: `unlikeTweet(address author, uint256 id) external` +- Allows a user to unlike a post they had previously liked. +- Emits a `postUnliked` event upon successful unliking. + +### 4. Get Post + +- **Function**: `getPost(uint _i) public view returns (Post memory)` +- Retrieves a specific post created by the calling user. + +### 5. Get All Posts + +- **Function**: `getAllPost(address _owner) public view returns (Post[] memory)` +- Retrieves all posts created by a specific user. + +### 6. Change Post Length + +- **Function**: `changePostLength(uint16 newPostLength) public onlyOwner` +- Allows the owner to change the maximum allowed post length. + +## Events + +- `postCreated(uint256 id, address author, string content, uint256 timestamp)` + - Emitted when a new post is created. + +- `postLiked(address liker, address postAuthor, uint256 postId, uint256 newLikeCount)` + - Emitted when a post is liked. + +- `postUnliked(address unliker, address postAuthor, uint256 postId, uint256 newLikedCount)` + - Emitted when a like on a post is removed. + +## Usage + +1. **Deployment** + - Deploy the contract using a compatible Ethereum development environment. + - Due to the issues I faced while trying to deploy my smart contract, I wrote an article on how I was able to overcome the challenge + - Here is the link to the article: https://medium.com/@aycrown77/resolving-a-getcontractfactory-error-in-hardhat-a-step-by-step-guide-d901fc186815 + +2. **Creating a Post** + - Use the `createPost` function to create a new post. + +3. **Liking a Post** + - Use the `likeTweet` function, providing the author's address and the post ID. + +4. **Unliking a Post** + - Use the `unlikeTweet` function, providing the author's address and the post ID. + +5. **Viewing Posts** + - Use the `getPost` or `getAllPost` functions to view posts. + +6. **Changing Post Length (Owner only)** + - Use the `changePostLength` function to adjust the maximum post length. + +## Notes + +- Ensure that the maximum post length (`MAX_POST_LENGHT`) is set to a reasonable value to prevent excessive gas costs for long posts. + +## Disclaimer + +- This smart contract is provided as-is. The owners and developers are not responsible for any issues, bugs, or misuse that may occur while using this contract. + +## License + +This smart contract is released under the [MIT License](LICENSE). diff --git a/web3_social_media/contracts/SocialMedial.sol b/web3_social_media/contracts/SocialMedial.sol new file mode 100644 index 00000000..ab79d731 --- /dev/null +++ b/web3_social_media/contracts/SocialMedial.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT +// web3 Social media smart contract +pragma solidity ^0.8.13; + +contract SocialMedia { + + uint16 public MAX_POST_LENGHT = 200; + address public owner; + + // define Post struct + struct Post { + uint256 id; + address author; + string content; + uint256 timestamp; + uint256 likes; + } + // maps user to post + mapping(address => Post[]) public posts; + + // Define the events + event postCreated(uint256 id, address author, + string content, uint256 timestamp); + event postLiked(address liker, address postAuthor, + uint256 postId, uint256 newLikeCount); + event postUnliked(address unliker, address postAuthor, + uint256 postId, uint256 newLikedCount); + + //constructor to set an owner of the contract + constructor() { + owner = msg.sender; + } + + //Modifier to verify the real owner + modifier onlyOwner() { + require(msg.sender == owner, "YOU ARE NOT THE OWNER OF THE CONTRACT"); + _; + } + + // Adds a post to the user + function createPost(string memory _post) public { + //check if post length <= 200, continue, otherwise revert + require(bytes(_post).length <= MAX_POST_LENGHT, "Post too long!"); + Post memory newPost = Post({ + id: posts[msg.sender].length, + author: msg.sender, + content: _post, + timestamp: block.timestamp, + likes: 0 + }); + posts[msg.sender].push(newPost); + + emit postCreated(newPost.id, newPost.author, newPost.content, newPost.timestamp); + } + + // like a post + function likeTweet(address author, uint256 id) external { + require(posts[author][id].id == id, "post does not exist"); + posts[author][id].likes++; + uint256 newLikeCount = posts[author][id].likes; + + emit postLiked(msg.sender, author, id, newLikeCount); + } + + // Unlike the post + function unlikeTweet(address author, uint256 id) external { + require(posts[author][id].id == id, "Post does not exist"); + require(posts[author][id].likes > 0, "Post has no likes"); + + posts[author][id].likes--; + + uint256 newLikeCount = posts[author][id].likes; + emit postUnliked(msg.sender, author, id, newLikeCount); + } + + // Gets post from the user + function getPost(uint _i) public view returns (Post memory){ + return posts[msg.sender][_i]; + } + + // Gets all post from the user + function getAllPost(address _owner) public view returns (Post[] memory){ + return posts[_owner]; + } + + // Changes the length of post + function changePostLength(uint16 newPostLength) public onlyOwner{ + MAX_POST_LENGHT = newPostLength; + } + +} \ No newline at end of file diff --git a/web3_social_media/hardhat.config.js b/web3_social_media/hardhat.config.js new file mode 100644 index 00000000..484ed51e --- /dev/null +++ b/web3_social_media/hardhat.config.js @@ -0,0 +1,11 @@ +require("@nomicfoundation/hardhat-toolbox"); + +/** @type import('hardhat/config').HardhatUserConfig */ +module.exports = { + solidity: "0.8.13", + networks: { + buildbear: { + url: "https://rpc.buildbear.io/marxist-rugor-nass-cbb8c77f", + }, + }, +}; \ No newline at end of file diff --git a/web3_social_media/package.json b/web3_social_media/package.json new file mode 100644 index 00000000..a13193c4 --- /dev/null +++ b/web3_social_media/package.json @@ -0,0 +1,18 @@ +{ + "name": "web3_social_media", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@openzeppelin/contracts": "^4.9.3" + }, + "devDependencies": { + "@nomicfoundation/hardhat-toolbox": "^1.0.2", + "hardhat": "^2.17.3" + } +} diff --git a/web3_social_media/scripts/deploy.js b/web3_social_media/scripts/deploy.js new file mode 100644 index 00000000..5c73b79e --- /dev/null +++ b/web3_social_media/scripts/deploy.js @@ -0,0 +1,17 @@ +const hre = require("hardhat"); + +async function main() { + const SocialMedia = await hre.ethers.getContractFactory("SocialMedia"); + const socialMedia = await SocialMedia.deploy(); + + await socialMedia.deployed(); + + console.log("SocialMedia deployed to:", socialMedia.address); + } + + main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); \ No newline at end of file diff --git a/web3_social_media/scripts/test/socialMedia.test.js b/web3_social_media/scripts/test/socialMedia.test.js new file mode 100644 index 00000000..369670df --- /dev/null +++ b/web3_social_media/scripts/test/socialMedia.test.js @@ -0,0 +1,48 @@ +const SocialMedia = artifacts.require("SocialMedia"); + +contract("SocialMedia", (accounts) => { + let socialMediaInstance; + + beforeEach(async () => { + socialMediaInstance = await SocialMedia.new(); + }); + + it("should create a post", async () => { + const content = "This is a test post."; + + await socialMediaInstance.createPost(content); + + const post = await socialMediaInstance.getPost(0); + assert.equal(post.content, content, "Post content does not match"); + }); + + it("should like a post", async () => { + const content = "This is another test post."; + await socialMediaInstance.createPost(content); + + await socialMediaInstance.likeTweet(accounts[0], 0); + + const post = await socialMediaInstance.getPost(0); + assert.equal(post.likes, 1, "Post should have 1 like"); + }); + + it("should unlike a post", async () => { + const content = "Yet another test post."; + await socialMediaInstance.createPost(content); + + await socialMediaInstance.likeTweet(accounts[0], 0); + await socialMediaInstance.unlikeTweet(accounts[0], 0); + + const post = await socialMediaInstance.getPost(0); + assert.equal(post.likes, 0, "Post should have 0 likes"); + }); + + it("should change post length", async () => { + const newPostLength = 300; + + await socialMediaInstance.changePostLength(newPostLength); + + const updatedPostLength = await socialMediaInstance.MAX_POST_LENGHT(); + assert.equal(updatedPostLength, newPostLength, "Post length should be updated"); + }); +}); diff --git a/web3_social_media/test/socialMedia.test.js b/web3_social_media/test/socialMedia.test.js new file mode 100644 index 00000000..369670df --- /dev/null +++ b/web3_social_media/test/socialMedia.test.js @@ -0,0 +1,48 @@ +const SocialMedia = artifacts.require("SocialMedia"); + +contract("SocialMedia", (accounts) => { + let socialMediaInstance; + + beforeEach(async () => { + socialMediaInstance = await SocialMedia.new(); + }); + + it("should create a post", async () => { + const content = "This is a test post."; + + await socialMediaInstance.createPost(content); + + const post = await socialMediaInstance.getPost(0); + assert.equal(post.content, content, "Post content does not match"); + }); + + it("should like a post", async () => { + const content = "This is another test post."; + await socialMediaInstance.createPost(content); + + await socialMediaInstance.likeTweet(accounts[0], 0); + + const post = await socialMediaInstance.getPost(0); + assert.equal(post.likes, 1, "Post should have 1 like"); + }); + + it("should unlike a post", async () => { + const content = "Yet another test post."; + await socialMediaInstance.createPost(content); + + await socialMediaInstance.likeTweet(accounts[0], 0); + await socialMediaInstance.unlikeTweet(accounts[0], 0); + + const post = await socialMediaInstance.getPost(0); + assert.equal(post.likes, 0, "Post should have 0 likes"); + }); + + it("should change post length", async () => { + const newPostLength = 300; + + await socialMediaInstance.changePostLength(newPostLength); + + const updatedPostLength = await socialMediaInstance.MAX_POST_LENGHT(); + assert.equal(updatedPostLength, newPostLength, "Post length should be updated"); + }); +});