0

Solution:

Array in Struct

Another post had a very similar issue and I encourage those with similar issues to give it a read.


I am attempting to create a basic social media smart contract with (seemingly) simple functionality. Users can create profiles, follow/unfollow other users, and create/delete posts. My intuition tells me to create a struct Profile and a struct Post that contain the fields of interest.

Initially, I wanted to add the attributes Post[] posts, Profile[] followers and Profile[] following inside of struct Profile, but I quickly found out that Solidity doesn't like this design pattern. I was lead down several hours worth of trying to determine what the best design pattern would be (in terms of optimizing for efficient creation, lookup and deletion of both Profile and Post objects).

The code I have here does indeed work, but I'm not sure if this design is optimal (though I severely doubt it). I would appreciate any feedback or suggestions on how to better improve this.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;

contract App {
    address[] internal addresses;
    mapping(string => address) internal names;
    mapping(address => Profile) public profiles;
    mapping(address => mapping(address => Profile)) internal followers;
    mapping(address => mapping(address => Profile)) internal following;
    mapping(address => Post[]) internal posts;

    // Profile
    struct Profile {
        address owner;
        string name;
        uint timeCreated;
        uint id;
        uint postCount;
        uint followerCount;
        uint followingCount;
        address[] followers;
        address[] following;
    }

    // Post
    struct Post {
        address author;
        string content;
        uint timeCreated;
        uint id;
    }

    // Check if message sender has a created profile
    modifier senderHasProfile() {
        require(profiles[msg.sender].owner != address(0x0), "ERROR: <Must create a profile to perform this action>");
        _;
    }

    // Check if a specified address has created a profile
    modifier profileExists(address _address) {
        require(profiles[_address].owner != address(0x0), "ERROR: <Profile does not exist>");
        _;
    }

    // Check that the message sender is not specifying an address that is itself
    modifier notSelf(address _address) {
        require(msg.sender != _address, "ERROR <You cannot follow/unfollow yourself");
        _;
    }

    // Check that the input is not empty
    modifier nonEmptyInput(string calldata _input) {
        require(keccak256(abi.encodePacked(_input)) != keccak256(abi.encodePacked("")), "ERROR: <Input cannot be empty>");
        _;
    }

    // Create a new profile from a given username
    function createProfile(string calldata _name) external nonEmptyInput(_name) {
        // Checks that the current account did not already make a profile and did not choose a taken username
        require(profiles[msg.sender].owner == address(0x0), "ERROR: <You have already created a profile>");
        require(names[_name] == address(0x0), "ERROR: <Username taken>");

        // Updates username list
        names[_name] = msg.sender;

        // Create the new profile object and add it to "profiles" mapping
        profiles[msg.sender] = Profile({
            owner: msg.sender,
            name: _name,
            timeCreated: block.timestamp,
            id: addresses.length,
            postCount: 0,
            followerCount: 0,
            followingCount: 0,
            followers: new address[](0x0),
            following: new address[](0x0)
        });

        // Add address to list of global addresses
        addresses.push(msg.sender);
    }

    // Follow a new profile
    function follow(address _address) external senderHasProfile profileExists(_address) notSelf(_address) {
        require(following[msg.sender][_address].owner == address(0x0), "ERROR: <You already follow this profile>");

        // Add entry to "followers" and "following" mappings
        followers[_address][msg.sender] = profiles[msg.sender];
        following[msg.sender][_address] = profiles[_address];

        // Add element to "followers" and "following" arrays in both Profile objects
        profiles[_address].followers.push(msg.sender);
        profiles[msg.sender].following.push(_address);

        // Increment "followerCount" and "followingCount" in both Profile objects
        profiles[_address].followerCount++;
        profiles[msg.sender].followingCount++;
    }

    // Unfollow a profile
    // This deletion operation has a time complexity of O(N), is there a better way to do this?
    function unfollow(address _address) external senderHasProfile profileExists(_address) notSelf(_address) {
        require(following[msg.sender][_address].owner != address(0x0), "ERROR: <You already do not follow this profile>");

        // Delete entry from "followers" and "following" mappings
        delete followers[_address][msg.sender];
        delete following[msg.sender][_address];

        // Delete element from "followers" array in Profile object and decrement followerCount
        for (uint i=0; i<profiles[_address].followerCount; i++) {
            if (profiles[_address].followers[i] == msg.sender) {
                delete profiles[_address].followers[i];
                profiles[_address].followerCount--;
                break;
            }
        }

        // Delete element from "following" array in Profile object and decrement followingCount
        for (uint i=0; i<profiles[msg.sender].followingCount; i++) {
            if (profiles[msg.sender].following[i] == _address) {
                delete profiles[msg.sender].following[i];
                profiles[msg.sender].followingCount--;
                break;
            }
        }
    }

    // Create a post
    function createPost(string calldata _content) external senderHasProfile() nonEmptyInput(_content) {
        // Create the Post object
        Post memory newPost = Post({
            author: msg.sender,
            content: _content,
            timeCreated: block.timestamp,
            id: posts[msg.sender].length
        });

        // Add entry to "posts" mappings
        posts[newPost.author].push(newPost);

        // Increment "postCount" in Profile object
        profiles[newPost.author].postCount++;
    }

    // Delete a post
    function deletePost(uint id) external senderHasProfile() {
        require(posts[msg.sender][id].author != address(0x0), "Post does not exist");

        delete posts[msg.sender][id];
        profiles[msg.sender].postCount--;
    }

    // Returns all posts from a given address
    function getPosts(address _address) external view profileExists(_address) returns(Post[] memory) {
        return posts[_address];
    }

    // Returns total user count
    function getUserCount() external view returns(uint) {
        return addresses.length;
    }

    // Returns all registered addresses
    function getAddresses() external view returns(address[] memory) {
        return addresses;
    }

    // Get all followers of a specific address
    function getFollowers(address _address) external view profileExists(_address) returns(address[] memory) {
        return profiles[_address].followers;
    }

    // Get all followed accounts of a specific address
    function getFollowing(address _address) external view returns(address[] memory) {
        return profiles[_address].following;
    }
}

1 Answer 1

0

Before you start writing contracts, you should think about the economics of your project. Now the cost of a transaction in public Ethereum is too high for its direct use as a system for storing/managing user posts - hardly anyone wants to pay $10+ per message. Therefore, first you need to decide on a public network with a suitable transaction price or start building your own public or private network. In the latter case, you need to figure out how you will attract node holders to your network. After all this, you will be able to decide for which network and in which language you will have to write smart contracts.

Exclusively as a demo version of your application, you can use public Ethereum test networks (Ropston, Rinkeby and others) or create your own private network on Ethereum, but you will still have to solve the above issues to implement your project in life.

There are several projects of this purpose - you can easily find them on the Internet.

1
  • Thank you for the suggestion. I am already aware of how expensive this smart contract would be to interact with on Ethereum mainnet. I'm only creating this smart contract for my own personal learning experience to get more familiar with the Solidity programming environment. Therefore it will be deployed on a local blockchain instead. Commented Nov 28, 2021 at 20:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.