Memeschain NFTs ✨ - Create, own, & sell your memes as NFTs on the ETH blockchain.

Memeschain NFTs ✨ - Create, own, & sell your memes as NFTs on the ETH blockchain.

#Netlify x #Hashnode #NetlifyHackathon submission.

What is Memeschain?

MemesChain is a web3 dApp that helps you create, own, and monetize your most creative memes as ERC-721 NFTs on the ETH blockchain.

NFT marketplaces are currently taking over the world with $2B in trading volume on Opensea just this month alone, but most of them are pretty generalized and not focused on any vertical or a community. Memeschain is the first marketplace for all your memes as NFTs that can be owned and sold on the blockchain.


Features 🎛

  • Register with your unique username tied to your ETH address. ✅
  • Connect Metamask wallet with your username ✅
  • Create meme(r) profile with avatar and username ✅
  • Smart contracts to mint, trade, sell, buy, authenticate NFTs on the ETH chain ✅
  • Browse all meme NFTs on the marketplace that is both on sale and owned. ✅
  • Create your own meme and convert it into an NFT ✅
  • Detect and prevent the minting of duplicate or plagiarised meme NFTs both off and on-chain ✅
  • Once minted, list the NFT on the user profile and the marketplace ✅
  • Allow off-chain community voting of meme NFTs with "DANK" (+1) or "BASIC" (-1) votes. ✅
  • Allow the ability to auction and trade the minted NFTs. ✅
  • Set dynamic auction price with an in-house oracle
  • Calculates the starting price of an NFT auction based on the popularity and number of "DANK" votes an NFT has. ✅
  • Enable buying, transfer, and relisting of NFTs that are on an auction if the auction price is met by the buyer. ✅

Architecture Diagram 🏛

Screenshot 2022-02-27 at 9.44.01 PM.png


Design 👨🏼‍🎨

The primary color used is a bright orange for the app with circular bold font.

Screenshot 2022-02-27 at 10.58.30 PM.png

Tech Stack 🛠️

Smart contracts

The ETH smart contracts are written in Solidity. The contracts have 12 different submodules shared between 2 deployment addresses. The core and the auction contracts. The contracts are compiled locally with solc and managed with Truffle. The API layer to contracts and storage is provided by Infura.io.

Front-end ➬

The client-side for the Memeschain NFT dApp is a web app built with React (CRA SPA) and the component library used is Antd with Craco. The router library used to manage the browser-side navigation is React-router v6. The data flow within the web app is by Redux. The Web3 wallet integration on the client-side is built with Web3JS and Truffle-HD-Wallet-Provider.

Back-end ↔️

The server-side for the Memeschain NFT dApp is a NodeJS app running Express and Parse Server. The off-chain storage, caching and queue is MongoDB managed by Atlas and AgendaJS. The NFT images are handled by Sharp. Duplicate detection is with a custom mix of pHash and fuzzball distance with AWS Rekognition. Reading data from the ETH blockchain is from Web3JS and Infura API.

Deployment 🛫

The backend is deployed on AWS with CI and CD managed by Github workflows. A combination of both EC2 and Docker container images orchestrated by K8's managed by Porter. The contracts are deployed with in-house scripts. The client-side is deployed on Netlify as a CRA SPA with CI and CD automatically provided out of the box from Netlify!

Screenshot 2022-02-27 at 10.42.48 PM.png


The dApp 💻

Sign in

The signing-in process to a web3 dApp is different from a traditional email and password experience. To access a dApp the user has to sign a request from the wallet that is available as a browser plugin as a security measure first. Here is how this is achieved with React + Web3JS + Redux.

UI state updated when the user has not signed via the wallet Screenshot 2022-02-27 at 2.47.03 PM.png

import Web3 from 'web3';
let web3;
if (window.ethereum) {
  web3 = new Web3(window.ethereum);
} else {
  const provider = new Web3.providers.HttpProvider(
    process.env.REACT_APP_INFURA_URL
  );
  web3 = new Web3(provider); 
}
export default web3;

Trigger the wallet for signing in

export const generateMetaMaskSign = (account, data) => {
  return new Promise((resolve, reject) => {
    window.web3.currentProvider.sendAsync({
      method: 'eth_signTypedData',
      params: [data, account],
      account,
    }, (err, { error, result }) => {
      if (err) {
        reject(err);
      }
      if (error) {
        reject(error.message);
      }
      resolve(result);
    });
  });
};

const sign = await generateMetaMaskSign(account, data);
await this.props.postUser({ sign, account, memer, avatar });

This brings up the web3 wallet prompt to sign the request

Screenshot 2022-02-27 at 2.46.34 PM.png

Detecting networks and wallet states.

Ethereum provides the ability to build and publish your contracts on the Main network as well as multiple test networks. The users of your dApps can also interact with these networks as well. So, it becomes vitally important to make sure to detect that the users are on the right network and on the right wallet state before using your dApp. Again this can be achieved with React + Web3JS + Redux.

Github Link - Here is the Redux action file that handles all the states of the Web3 wallet

Screenshot 2022-02-27 at 3.10.03 PM.png

Creating and minting meme NFTs

Screenshot 2022-02-27 at 4.20.05 PM.png

Minting involves fine-grained interactions between the client UI + Web3, smart contracts, and the server.

Solidity method to mint a meme NFT

    function _createMeme(
        bytes _hash,
        bytes _dId
    )
        internal
        returns (uint)
    {

        // Make sure no duplicate hashes are sent here.

        Meme memory _meme = Meme({
            hash: _hash,
            dId: _dId,
            creationTime: uint64(now)
        });
        uint256 newMemeId = memes.push(_meme) - 1;
        require(newMemeId == uint256(uint32(newMemeId)));

        // emit the MemeCreated event
        MemeCreated(
            newMemeId,
            _hash
        );

        memeHashToExists[_hash] = true;
        memeDIdToExists[_dId] = true;

        // ERC721 transfer
        _transfer(0, msg.sender, newMemeId);

        return newMemeId;
    }

The above method invoked by the user Web3 wallet

Screenshot 2022-02-27 at 5.51.57 PM.png

Preventing duplicate or plagiarized memes from minting

Since creators put in a lot of effort to create witty and clever meme NFTs which can be later auctioned or sold on the platform puts an incentive for users to copy the most popular memes. To prevent this we have come up with an algorithmic + AI solution that produces a duplicate score. Anything above 60 is not worth minting.

  • Generating pHash on NFTs with Sharp-pHash to compare the hamming distance
    const difference = pHash.hammingDistance(hashToCompare, meme.get('hash'));
    
  • Fuzzy match average on the words and sentences in a meme

    fuzzyMatchAverage(str1, str2) {
      const { inStr1, inStr2 } = strSanitize({ inStr1: str1, inStr2: str2 });
      if (!inStr1 && !inStr2) {
        return 100;
      }
    
      if (!inStr1 || !inStr2) {
        return 0;
      }
      const ratio = fuzz.ratio(inStr1, inStr2);
      const partialRatio = fuzz.partial_ratio(inStr1, inStr2);
      const tokenSortRatio = fuzz.token_sort_ratio(inStr1, inStr2);
      const tokenSetRatio = fuzz.token_set_ratio(inStr1, inStr2);
      return (ratio + partialRatio + tokenSortRatio + tokenSetRatio) / 4;
    },
    };
    
  • Using AWS Rekognition to detect the words and sentences used in conjunction with pHash's hamming distance and fuzzball ratio

Using the combination of AI, hashing, and Levenshtein distance with Fuzzballs we predict the possibility of the meme is a duplicate. This prediction is very nuanced and has a lot of edge cases like meme template is the same but the text in the meme being worded differently, etc.

Let’s dig deep into it. As an owner of a meme, you have the ability to put your meme up for auction. But there is a catch. The starting price of your auction will be 0.001 times the number of DANK votes your meme has. Higher the number of DANK votes higher will be your starting price.

So, if your meme has 15 upvotes, you get to put it up for auction at 0.015 ETH. BAM! That’s some kind of dough you get for popularity.

If anybody wants to buy your meme which is at auction, they have to pay up whatever the current auction price is. As a part of owning the meme, the new owner will get upvotes along with the meme. If he/she decides to sell it, the price will always be greater than that of what the previous owner sold it plus the number of votes the new owner has managed to get.

Off-chain NFT voting

To deduce a meme NFTs popularity and to indicate prices for the auction contract, an off-chain voting mechanism has been built in for authenticated users, which acts as an oracle to detect suitable auction prices for the NFT.

Screenshot 2022-02-27 at 8.52.23 PM.png

On-chain auction

Auctions, specifically dutch are the trading mechanism for buying and selling NFTs on the platform. The auctions and the sale modules require the complete tech stack interaction.

  • The voting oracles predict the price for the auction off-chain based on the number of upvotes

  • The client UI Web3JS triggers the auction contract methods with the data from the oracle.

  • The actual solidity contract moves the NFT from the seller's wallet to the contract's wallet and holds it with price rules factoring out equally until the auction time runs out.

Contract that puts a meme NFT up for auction

    function createSaleAuction(
        uint256 _memeId,
        uint256 _startingPrice,
        uint256 _endingPrice,
        uint256 _duration
    )
        external
        whenNotPaused
    {
        // Auction contract checks input sizes
        // If meme is already on any auction, this will throw
        // because it will be owned by the auction contract.
        require(_owns(msg.sender, _memeId));

        _approve(_memeId, saleAuction);
        // Sale auction throws if inputs are invalid and clears
        // transfer and sire approval after escrowing the meme.
        saleAuction.createAuction(
            _memeId,
            _startingPrice,
            _endingPrice,
            _duration,
            msg.sender
        );
    }

    /// @dev Transfers the balance of the sale auction contract
    /// to the MemesChainCore contract. We use two-step withdrawal to
    /// prevent two transfer calls in the auction bid function.
    function withdrawAuctionBalances() external onlyCLevel {
        saleAuction.withdrawBalance();
    }

Trigger auctions from Web3JS

export const auctionMeme = ({ memeRefId, memeId, startingPrice, endingPrice, endingAuctionTime }) => async (dispatch) => {
  try {
    dispatch({
      type: CREATE_MEME_AUCTION_LOADING,
      payload: {},
    });

    const now = moment(new Date());
    const ending = moment(endingAuctionTime); // Already a moment instance
    const duration = ending.diff(now, 'seconds');
    if (duration < 60) {
      throw new Error('Minimum auction time is 1 minute');
    }

    // This should be the first before any eth call function to be called
    const account = await validateProvider();

    // Ignore the coming param of startingPrice because user might have tampered with it
    // using tricks like inspect ele
    const { data: priceAccToVote } = await Parse.Cloud.run('get_price', { memeRefId });

    if (endingPrice > priceAccToVote) {
      throw new Error('Ending price should be less than starting price. If error persists please refresh page.');
    }

    await MemesChainCore.methods.createSaleAuction(
      memeId,
      web3.utils.toWei(priceAccToVote.toString(), 'ether'),
      web3.utils.toWei(endingPrice.toString(), 'ether'),
      duration,
    ).send({
      from: account,
      value: web3.utils.toWei('0', 'ether'),
      gasPrice: await web3.eth.getGasPrice(),
    })
    .on('transactionHash', () => message.info('Transaction obtained. Waiting for confirmation. This might take upto 15 seconds...'));

    dispatch({
      type: CREATE_MEME_AUCTION_SUCCESS,
      payload: {},
    });

  } catch (e) {
    const { code, message } = handleParseError(e);
    dispatch({
      type: CREATE_MEME_AUCTION_ERROR,
      payload: {
        message: e.message ? e.message.split('\n')[0] : message,
      },
    });
  }
}

Screenshot 2022-02-27 at 8.54.08 PM.png

Marketplace

The marketplace is a section within the app that basically acts as a snapshot timeline of all the NFTs on the blockchain. The marketplace listing is the server-side workhorse module that reads data from the ETH blockchain using the Node Web3JS library connected to the Infura blockchain and maps the NFT hash and id data with the Mongo Database for efficient and cheaper storage strategies.

Screenshot 2022-02-27 at 8.55.28 PM.png

The Node server app exposes an API to the client that lists all the NFTs that are on the marketplace or on auction to the client by reading the blockchain, mapping it to Mongo, and then caching the results until the next NFT has been minted on the platform.

  async get_auctions(req, res) {
    try {
      const { tokensOnAuction: tokens, total } = await getTokens({ memer, offset, limit });
      const memesWithAuctionFromETH = await Promise.all(tokens.map(async ({ memeId: memeOnAuctionId, ...auction }) => {
        const {
          hash,
          dId,
          owner,
          memeId,
        } = await ETHMeme.ETH_getMeme({ memeId: memeOnAuctionId, auction });
        return {
          hash,
          dId,
          owner,
          memeId,
          auction,
        };
      }).filter(Boolean));
      const memesFromAuction = await dbUtils.getMemesFromDB(memesWithAuctionFromETH, user);
      return({
        data: {
          result: memesFromAuction.filter(Boolean).map(({
            objectId,
            createdAt: memeCreatedAt,
            updatedAt,
            owner: { createdAt, objectId: ownerObjectId, updatedAt: ownerUpdateAt, ...owner },
            ...rest
          }) => ({ ...rest, owner, memeRefId: oneTimeEncryption(objectId) })),
          total,
        },
      });
    } catch (e) {
      errors.handleError(errors.constructErrorObject(e.code || 500, e), res);
    }
  }

Responsive design is supported with media queries 🔮

BeFunky-collage (1).jpg

I have put my best efforts to make the architecture diagram as descriptive and elaborate as possible so the need to write it down further is reduced.


Recipes 🥘

Detect words and sentences from a meme NFT image

     rekognition.detectText({
        Image: {
          Bytes: buffer,
        },
      }, (err, { TextDetections: result } = {}) => {
        if (err) {
          reject(err);
        } else {
          const dirtyString = result.reduce((prev, {
            Confidence,
            Type,
            DetectedText,
            Geometry: { BoundingBox: { Height, Width } },
          }) => {

            if (Height > MIN_BOUNDING_BOX && Width > MIN_BOUNDING_BOX) {
              if (Confidence >= 70 && Type === 'LINE') {
                prev += DetectedText + ' ';
              }
            }
            return prev;
          }, '').trim();

          const words = result.map(({
            Confidence,
            Type,
            DetectedText,
            Geometry: { BoundingBox: { Height, Width } },
          }) => {
            if (Height > MIN_BOUNDING_BOX && Width > MIN_BOUNDING_BOX) {
              if (Confidence >= 70 && Type === 'WORD') {
                return DetectedText.trim().toLowerCase();
              }
            }
            return false;
          }).filter(Boolean);

          const { sentence } = strSanitize({ sentence: dirtyString });

Encryption and decryption for web3 wallet sign

const crypto = require('crypto');

const ENCRYPTION_KEY = process.env.MULTI_ENCRYPTION_KEY;
const IV_LENGTH = 16; // For AES, this is always 16

function encrypt(text) {
  let iv = crypto.randomBytes(IV_LENGTH);
  let cipher = crypto.createCipheriv('aes-256-cbc', new Buffer(ENCRYPTION_KEY), iv);
  let encrypted = cipher.update(text);

  encrypted = Buffer.concat([encrypted, cipher.final()]);

  return iv.toString('hex') + ':' + encrypted.toString('hex');
}

function decrypt(text) {
  let textParts = text.split(':');
  let iv = new Buffer(textParts.shift(), 'hex');
  let encryptedText = new Buffer(textParts.join(':'), 'hex');
  let decipher = crypto.createDecipheriv('aes-256-cbc', new Buffer(ENCRYPTION_KEY), iv);
  let decrypted = decipher.update(encryptedText);

  decrypted = Buffer.concat([decrypted, decipher.final()]);

  return decrypted.toString();
}

Sharp image normalization to apply the pHash distance equally

const sharp = require('sharp');

module.exports = {
  toJPEG(buffer) {
    return new Promise((resolve, reject) => {
      sharp(buffer).flatten().trim(10).jpeg()
        .toBuffer({ resolveWithObject: true })
        .then(({ data, info: { width = 0, height = 0 } }) => resolve({ data, width, height }))
        .catch(err => reject(err));
    });
  },
};

Real-time ETH to USD with Coinbase

const Client = require('coinbase').Client;
require('dotenv').config();
const apiKey = process.env.COINBASE_API_KEY;
const apiSecret = process.env.COINBASE_API_SECRET_KEY;
const CustomError = require('../custom-error/index');

var client = new Client({ apiKey, apiSecret });

module.exports = {
  tenCentsToEth() {
    return new Promise((resolve, reject) => {
      try {
        client.getSpotPrice({'currencyPair': 'ETH-USD'}, (err, result) => {
          if (err) {
            reject(err);
          }
          const { data: { amount } } = result;
          resolve(((1 / (+amount)) * 0.10).toFixed(18));
        });
      } catch (e) {
        throw new CustomError(500, e);
      }
    })
  }
}

Challenges ⛰

There were multiple coding challenges that cannot be covered in this article alone. But a common gist of most of the issues was this. Since the web3 platform and all the libraries are constantly evolving during the course of the project, a lot of code written at the start of the project would fail to work when the library was updated to support more features. This made the development task challenging.

One problem I got stuck on was the mapping variables from ETH were not exposed to Web3JS at first glance. I had to figure out few things before I got it working from this SO thread


Good engineering practices 🧑🏽‍💻

  • ✅ Linting by ESLint
  • ✅ Prettier syntax indentation.
  • ✅ Redux data flow throughout the app with reducers, actions, types, and store.
  • ✅ Easy to understand folder structure for the React JSX with components, routes, utils, services, and more.
  • ✅ Simple and reusable component design.
  • ✅ Personal framework for the ExpressJS + ParseJS backend with SOLID coding principles.
  • ✅ Tested smart contracts.

Application available on 🌍

Memeschain.com


Github repo for the Netlify hosted module 🧑🏽‍💻

Memeschain Client Github Repo Note: The contracts and the API layer are private as of now


Connect with me 😀