[Chainlink] How To Create an NFT Game

In this tutorial, we’ll be building an NFT-based Web3 “game.” By “game,” I mean a Tamagotchi-like dApp that is 100% stored on the blockchain.

We will store all assets and everything regarding this dApp on the blockchain. Let’s take a look at what we’ll be building. The web app portion of the dApp is pretty simple. It’s going to give us access to our EmojiGotchi.

Image of an emoji NFT on blue background

Starter Project Files

You can find the starter for this project here.

It’s broken into three different branches. To start, we’ll be in the main branch. This is the best place to start. The repo is split into three different branches, and once we’ve built the contract out, the code should match the foundry branch. Once you build out the complete application, it should be the same as the final branch. This branch houses the completed application. 

We are going to be using Foundry. Foundry is a new toolkit for Ethereum development. Foundry is fast, and the tests are written in Solidity. Not having to switch context between Javascript and Solidity when writing tests makes for a good user experience, and the contract code is excellent!

You can find the installation instruction for Foundry in the documentation.

You will also be using SvelteKit for the frontend. I am a fan of SvelteKit as it’s easy to explain what’s going on. There’s a little bit of “Svelte magic” that you’ll get into, but overall it’s pretty straightforward as a place to start. Let’s dive in.

The project is set up as a mono repo. There are two subfolders, foundry and svelte, and a bit of VS Code Magic if you choose to use that editor. The workbench title bar will be colored based on which portion of the project you are in.

When comparing the base install of Foundry and SvelteKit, there are a couple of additional tools I’ve set up.

deploy.sh

#!/usr/bin/env bash

# Read the Mumbai RPC URL
echo Enter Your Mumbai RPC URL:
echo Example: "https://polygon-mumbai.g.alchemy.com/v2/XXXXXXXXXX"
read -s rpc

# Read the contract name
echo Which contract do you want to deploy \(eg Greeter\)?
read contract

forge create ./src/${contract}.sol:${contract} -i --rpc-url $rpc

This will let you deploy our contract to Mumbai, and all this is doing is reading variables without displaying what you’re typing in on the command line. This will ensure your private keys aren’t stored in your command line history.

remappings.txt

@openzeppelin/=lib/openzeppelin-contracts/

ds-test/=lib/ds-test/src/

This remapping file lets you use things like OpenZeppelin contracts and import them in the way you would typically use other tools such as Hardhat or Remix. This file remaps the import to the directory where they are housed. I’ve also installed the OpenZeppelin contracts via forge install openzeppelin/openzeppelin-contracts These will be used to create the ERC-721 contract.

Start Building The dApp

EmojiGotchi Screenshot

Let’s take another look at EmojiGotchi. There are a few things to think about here. First, the image is an SVG that is stored on-chain. You’ve got a few different values to track: hunger, enrichment, and happiness. You’ve got two different things you can do: You can feed it and play with it. Let’s stub out some tests. If you look within the Foundry directory, you have your src directory, and inside src, you have a couple of things.

Foundry provides us with a basic contract and test. This enables us to run forge test

Forge is one of the commands within Foundry. When we run forge test, it compiles our contract and it runs our test, and you can see that our tests passed.

❯ foundry (main) ✘ forge test

[⠒] Compiling...
No files changed, compilation skipped

Running 1 test for src/test/Contract.t.sol:ContractTest
[PASS] testExample() (gas: 190)
Test result: ok. 1 passed; 0 failed; finished in 222.21µs
❯ foundry (main) ✘

This also shows that you have installed Foundry correctly, and you can continue to build out the contract.

Let’s go ahead and add a new file, EmojiGotchi.t.sol. The t shows that it’s a test.

src/test/EmojiGotchi.t.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.13;

import "../EmojiGotchi.sol";
import "ds-test/test.sol";

contract EmojiGotchiTest is DSTest {
     EmojiGotchi public eg;

    function setUp() public {}

    function testExample() public {
        assertTrue(true);
    }

}

You are importing the contract, which you haven’t yet created, as well as ds-test/test.sol

Let’s also create EmojiGotchi.sol in the src directory. You can create it as an empty contract to ensure that everything is working. 

src/EmojiGotchi.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.13;

contract EmojiGotchi {}

So we’ve got our test. It doesn’t test anything yet, but if you go ahead and run it via forge test, you’ll see two test examples. This lets us know everything is set up and working. 

❯ foundry (main) ✘ forge test

[⠢] Compiling...
Compiler run successful

Running 1 test for src/test/EmojiGotchi.t.sol:EmojiGotchiTest
[PASS] testExample() (gas: 212)
Test result: ok. 1 passed; 0 failed; finished in 1.85ms

Running 1 test for src/test/Contract.t.sol:ContractTest
[PASS] testExample() (gas: 190)
Test result: ok. 1 passed; 0 failed; finished in 1.85ms

❯ foundry (main) ✘

Clean Up

At this point, you can clean up the Contract.sol contract and tests. 

Delete src/Contract.sol and src/test/Contract.t.sol

Writing the First Real Test

You’ve got your test file Contract.t.sol, which has a test to ensure that true is true. When you run it, it’s currently just ensuring that our contract can be imported and that everything is running as expected. 

Let’s take a moment and think about the EmojiGotchi again and note what we want it to do. This will provide a set of tests that we need to pass. 

- mint the EmojiGotchi NFT
- set the metadata
- pass time
- feed the EmojiGotchi 
- play with the EmojiGotchi
- change the image based on happiness
- check if upkeep is needed
- perform upkeep if needed

This list of tests should cover the basic functionality of the EmojiGotchi. If you head back to src/test/EmojiGotchi.t.sol, you can flesh out the first test, testMint().

src/test/EmojiGotchi.t.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.13;

import "../EmojiGotchi.sol";
import "ds-test/test.sol";

contract EmojiGotchiTest is DSTest {
    EmojiGotchi public eg;

    function setUp() public {
            eg = new EmojiGotchi();
            address addr = 0x1234567890123456789012345678901234567890;
            eg.safeMint(addr);
}

function testMint() public {
      address addr = 0x1234567890123456789012345678901234567890;
      address owner = eg.ownerOf(0);
      assertEq(addr, owner);
}

}

You will need to modify the setUp() function. This function is run before every test, and in this case, the actual minting will occur in the setUp() function. 

In the testMint() function, you can verify that you minted an NFT in the setUp()function and that it’s assigned to the expected owner. 

If you run this as-is, you will see an error.

Error: 
   0: Compiler run failed
      TypeError: Member "safeMint" not found or not visible after argument-dependent lookup in contract EmojiGotchi.
        --> 
/Users/rg/Development/EmojiGotchi/foundry/src/test/EmojiGotchi.t.sol:13:9:
         |
      13 |         eg.safeMint(addr);
         |         ^^^^^^^^^^^

This is expected as we haven’t put anything in our EmojiGotchi.sol contract yet. 

A great place to start for NFTs is the OpenZeppelin Wizard, which provides an industry-standard skeleton to build off of. That is where we will be starting for the EmojiGotchi contract.

src/EmojiGotchi.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract EmojiGotchi is ERC721, ERC721URIStorage {
    using Counters for Counters.Counter;

    Counters.Counter private _tokenIdCounter;

    constructor() ERC721("EmojiGotchi", "emg") {}

    function safeMint(address to) public {
        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, tokenURI(tokenId));
    }

    // The following functions are overrides required by Solidity.

    function _burn(uint256 tokenId)
        internal
        override(ERC721, ERC721URIStorage)
    {
        super._burn(tokenId);
    }

    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

}

If you rerun your test using forge test, you will see you can successfully mint an NFT!

❯ foundry (main) ✘ forge test


[⠊] Compiling...
[⠆] Compiling 2 files with 0.8.13
[⠔] Solc finished in 218.31ms
Compiler run successful

Running 1 test for src/test/EmojiGotchi.t.sol:EmojiGotchiTest
[PASS] testMint() (gas: 7844)

Test result: ok. 1 passed; 0 failed; finished in 2.71ms

Testing the Metadata 

This next test seems simple at first, but you will be building out a massive part of the contract to ensure it’s working. Add a new test: testUri()

src/test/EmojiGotchi.t.sol

function testUri() public {
(uint256 happiness, uint256 hunger, uint256 enrichment, uint256 checked, ) = eg.gotchiStats(
    0
    );
    assertEq(happiness, (hunger + enrichment) / 2);
    assertEq(hunger, 100);
    assertEq(enrichment, 100);
    assertEq(checked, block.timestamp);

}

This test will check that your contract has a function, gotchiStats, which will return the happiness, hunger, enrichment stats for your NFT, plus the last time you checked for your NFT. 

An Aside About SVGs

For the image portion of the EmojiGotchi, you will be using an SVG. In its raw form it will looking something like this:

<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox='0 0 800 800'>
    <rect fill="#ffffff" width="800" height="800" />
    <defs>
      <radialGradient id='a' cx='400' cy='400' r="50.1%" gradientUnits="userSpaceOnUse">
        <stop offset="0" stop-color="#ffffff" />
        <stop offset="1" stop-color="#0EF" />
      </radialGradient>
      <radialGradient id='b' cx='400' cy='400' r="50.4%" gradientUnits="userSpaceOnUse">
        <stop offset="0" stop-color="#ffffff" />
        <stop offset="1" stop-color="#0FF" />
      </radialGradient>
    </defs>
    <rect fill="url(#a)" width="800" height="800" />
    <g fill-opacity='0.5'>
      <path fill="url(#b)" d='M998.7 439.2c1.7-26.5 1.7-52.7 0.1-78.5L401 399.9c0 0 0-0.1 0-0.1l587.6-116.9c-5.1-25.9-11.9-51.2-20.3-75.8L400.9 399.7c0 0 0-0.1 0-0.1l537.3-265c-11.6-23.5-24.8-46.2-39.3-67.9L400.8 399.5c0 0 0-0.1-0.1-0.1l450.4-395c-17.3-19.7-35.8-38.2-55.5-55.5l-395 450.4c0 0-0.1 0-0.1-0.1L733.4-99c-21.7-14.5-44.4-27.6-68-39.3l-265 537.4c0 0-0.1 0-0.1 0l192.6-567.4c-24.6-8.3-49.9-15.1-75.8-20.2L400.2 399c0 0-0.1 0-0.1 0l39.2-597.7c-26.5-1.7-52.7-1.7-78.5-0.1L399.9 399c0 0-0.1 0-0.1 0L282.9-188.6c-25.9 5.1-51.2 11.9-75.8 20.3l192.6 567.4c0 0-0.1 0-0.1 0l-265-537.3c-23.5 11.6-46.2 24.8-67.9 39.3l332.8 498.1c0 0-0.1 0-0.1 0.1L4.4-51.1C-15.3-33.9-33.8-15.3-51.1 4.4l450.4 395c0 0 0 0.1-0.1 0.1L-99 66.6c-14.5 21.7-27.6 44.4-39.3 68l537.4 265c0 0 0 0.1 0 0.1l-567.4-192.6c-8.3 24.6-15.1 49.9-20.2 75.8L399 399.8c0 0 0 0.1 0 0.1l-597.7-39.2c-1.7 26.5-1.7 52.7-0.1 78.5L399 400.1c0 0 0 0.1 0 0.1l-587.6 116.9c5.1 25.9 11.9 51.2 20.3 75.8l567.4-192.6c0 0 0 0.1 0 0.1l-537.3 265c11.6 23.5 24.8 46.2 39.3 67.9l498.1-332.8c0 0 0 0.1 0.1 0.1l-450.4 395c17.3 19.7 35.8 38.2 55.5 55.5l395-450.4c0 0 0.1 0 0.1 0.1L66.6 899c21.7 14.5 44.4 27.6 68 39.3l265-537.4c0 0 0.1 0 0.1 0L207.1 968.3c24.6 8.3 49.9 15.1 75.8 20.2L399.8 401c0 0 0.1 0 0.1 0l-39.2 597.7c26.5 1.7 52.7 1.7 78.5 0.1L400.1 401c0 0 0.1 0 0.1 0l116.9 587.6c25.9-5.1 51.2-11.9 75.8-20.3L400.3 400.9c0 0 0.1 0 0.1 0l265 537.3c23.5-11.6 46.2-24.8 67.9-39.3L400.5 400.8c0 0 0.1 0 0.1-0.1l395 450.4c19.7-17.3 38.2-35.8 55.5-55.5l-450.4-395c0 0 0-0.1 0.1-0.1L899 733.4c14.5-21.7 27.6-44.4 39.3-68l-537.4-265c0 0 0-0.1 0-0.1l567.4 192.6c8.3-24.6 15.1-49.9 20.2-75.8L401 400.2c0 0 0-0.1 0-0.1L998.7 439.2z' />
    </g>
    <text x='50%' y='50%' class="base" dominant-baseline="middle" text-anchor="middle" font-size="8em">🤩</text>
</svg>

Everything up to the <text> toward the end of the SVG will stay the same as the happiness level of the EmojiGotchi changes. The emoji within the <text> is the dynamic portion of the SVG. You will be updating it based on the happiness level of the EmojiGotchi. 

Adding SVGs, Structs, and Mappings

When you store this information inside the token URI, it needs to be base64 encoded. OpenZeppelin provides a library to accomplish this. In this example, you will be using a pre-encoded SVG version. Add these variables to the body of the contract.

src/EmojiGotchi.sol

string SVGBase =

'data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxMDAlJyBoZWlnaHQ9JzEwMCUnIHZpZXdCb3g9JzAgMCA4MDAgODAwJz48cmVjdCBmaWxsPScjZmZmZmZmJyB3aWR0aD0nODAwJyBoZWlnaHQ9JzgwMCcvPjxkZWZzPjxyYWRpYWxHcmFkaWVudCBpZD0nYScgY3g9JzQwMCcgY3k9JzQwMCcgcj0nNTAuMSUnIGdyYWRpZW50VW5pdHM9J3VzZXJTcGFjZU9uVXNlJz48c3RvcCAgb2Zmc2V0PScwJyBzdG9wLWNvbG9yPScjZmZmZmZmJy8+PHN0b3AgIG9mZnNldD0nMScgc3RvcC1jb2xvcj0nIzBFRicvPjwvcmFkaWFsR3JhZGllbnQ+PHJhZGlhbEdyYWRpZW50IGlkPSdiJyBjeD0nNDAwJyBjeT0nNDAwJyByPSc1MC40JScgZ3JhZGllbnRVbml0cz0ndXNlclNwYWNlT25Vc2UnPjxzdG9wICBvZmZzZXQ9JzAnIHN0b3AtY29sb3I9JyNmZmZmZmYnLz48c3RvcCAgb2Zmc2V0PScxJyBzdG9wLWNvbG9yPScjMEZGJy8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHJlY3QgZmlsbD0ndXJsKCNhKScgd2lkdGg9JzgwMCcgaGVpZ2h0PSc4MDAnLz48ZyBmaWxsLW9wYWNpdHk9JzAuNSc+PHBhdGggZmlsbD0ndXJsKCNiKScgZD0nTTk5OC43IDQzOS4yYzEuNy0yNi41IDEuNy01Mi43IDAuMS03OC41TDQwMSAzOTkuOWMwIDAgMC0wLjEgMC0wLjFsNTg3LjYtMTE2LjljLTUuMS0yNS45LTExLjktNTEuMi0yMC4zLTc1LjhMNDAwLjkgMzk5LjdjMCAwIDAtMC4xIDAtMC4xbDUzNy4zLTI2NWMtMTEuNi0yMy41LTI0LjgtNDYuMi0zOS4zLTY3LjlMNDAwLjggMzk5LjVjMCAwIDAtMC4xLTAuMS0wLjFsNDUwLjQtMzk1Yy0xNy4zLTE5LjctMzUuOC0zOC4yLTU1LjUtNTUuNWwtMzk1IDQ1MC40YzAgMC0wLjEgMC0wLjEtMC4xTDczMy40LTk5Yy0yMS43LTE0LjUtNDQuNC0yNy42LTY4LTM5LjNsLTI2NSA1MzcuNGMwIDAtMC4xIDAtMC4xIDBsMTkyLjYtNTY3LjRjLTI0LjYtOC4zLTQ5LjktMTUuMS03NS44LTIwLjJMNDAwLjIgMzk5YzAgMC0wLjEgMC0wLjEgMGwzOS4yLTU5Ny43Yy0yNi41LTEuNy01Mi43LTEuNy03OC41LTAuMUwzOTkuOSAzOTljMCAwLTAuMSAwLTAuMSAwTDI4Mi45LTE4OC42Yy0yNS45IDUuMS01MS4yIDExLjktNzUuOCAyMC4zbDE5Mi42IDU2Ny40YzAgMC0wLjEgMC0wLjEgMGwtMjY1LTUzNy4zYy0yMy41IDExLjYtNDYuMiAyNC44LTY3LjkgMzkuM2wzMzIuOCA0OTguMWMwIDAtMC4xIDAtMC4xIDAuMUw0LjQtNTEuMUMtMTUuMy0zMy45LTMzLjgtMTUuMy01MS4xIDQuNGw0NTAuNCAzOTVjMCAwIDAgMC4xLTAuMSAwLjFMLTk5IDY2LjZjLTE0LjUgMjEuNy0yNy42IDQ0LjQtMzkuMyA2OGw1MzcuNCAyNjVjMCAwIDAgMC4xIDAgMC4xbC01NjcuNC0xOTIuNmMtOC4zIDI0LjYtMTUuMSA0OS45LTIwLjIgNzUuOEwzOTkgMzk5LjhjMCAwIDAgMC4xIDAgMC4xbC01OTcuNy0zOS4yYy0xLjcgMjYuNS0xLjcgNTIuNy0wLjEgNzguNUwzOTkgNDAwLjFjMCAwIDAgMC4xIDAgMC4xbC01ODcuNiAxMTYuOWM1LjEgMjUuOSAxMS45IDUxLjIgMjAuMyA3NS44bDU2Ny40LTE5Mi42YzAgMCAwIDAuMSAwIDAuMWwtNTM3LjMgMjY1YzExLjYgMjMuNSAyNC44IDQ2LjIgMzkuMyA2Ny45bDQ5OC4xLTMzMi44YzAgMCAwIDAuMSAwLjEgMC4xbC00NTAuNCAzOTVjMTcuMyAxOS43IDM1LjggMzguMiA1NS41IDU1LjVsMzk1LTQ1MC40YzAgMCAwLjEgMCAwLjEgMC4xTDY2LjYgODk5YzIxLjcgMTQuNSA0NC40IDI3LjYgNjggMzkuM2wyNjUtNTM3LjRjMCAwIDAuMSAwIDAuMSAwTDIwNy4xIDk2OC4zYzI0LjYgOC4zIDQ5LjkgMTUuMSA3NS44IDIwLjJMMzk5LjggNDAxYzAgMCAwLjEgMCAwLjEgMGwtMzkuMiA1OTcuN2MyNi41IDEuNyA1Mi43IDEuNyA3OC41IDAuMUw0MDAuMSA0MDFjMCAwIDAuMSAwIDAuMSAwbDExNi45IDU4Ny42YzI1LjktNS4xIDUxLjItMTEuOSA3NS44LTIwLjNMNDAwLjMgNDAwLjljMCAwIDAuMSAwIDAuMSAwbDI2NSA1MzcuM2MyMy41LTExLjYgNDYuMi0yNC44IDY3LjktMzkuM0w0MDAuNSA0MDAuOGMwIDAgMC4xIDAgMC4xLTAuMWwzOTUgNDUwLjRjMTkuNy0xNy4zIDM4LjItMzUuOCA1NS41LTU1LjVsLTQ1MC40LTM5NWMwIDAgMC0wLjEgMC4xLTAuMUw4OTkgNzMzLjRjMTQuNS0yMS43IDI3LjYtNDQuNCAzOS4zLTY4bC01MzcuNC0yNjVjMCAwIDAtMC4xIDAtMC4xbDU2Ny40IDE5Mi42YzguMy0yNC42IDE1LjEtNDkuOSAyMC4yLTc1LjhMNDAxIDQwMC4yYzAgMCAwLTAuMSAwLTAuMUw5OTguNyA0MzkuMnonLz48L2c+PHRleHQgeD0nNTAlJyB5PSc1MCUnIGNsYXNzPSdiYXNlJyBkb21pbmFudC1iYXNlbGluZT0nbWlkZGxlJyB0ZXh0LWFuY2hvcj0nbWlkZGxlJyBmb250LXNpemU9JzhlbSc+8J+';

    string[] emojiBase64 = [

        'kqTwvdGV4dD48L3N2Zz4=',

        'YgTwvdGV4dD48L3N2Zz4=',

        'YkDwvdGV4dD48L3N2Zz4=',

        'YoTwvdGV4dD48L3N2Zz4=',

        'SgDwvdGV4dD48L3N2Zz4='

    ];

SVGBase is a string that contains all of the encoded SVG up until the emoji itself.

emojiBase64 is an array of the different emojis and closes the SVG string. 

In addition to the SVG variables, you will need to create a struct for the attributes of the EmojiGotchi, GotchiAttributs, as well as a mapping of NFT holders to their token id, gotchiHolders, and a mapping from the token id to the attributes, gotchiHolderAttributes.

src/EmojiGotchi.sol

    struct GotchiAttributes {
        uint256 gotchiIndex;
        string imageURI;
        uint256 happiness;
        uint256 hunger;
        uint256 enrichment;
        uint256 lastChecked;
    }

mapping(address => uint256) public gotchiHolders;

mapping(uint256 => GotchiAttributes) public gotchiHolderAttributes;

Updating The Minting Process

The next step is to update the safeMint() function to include the metadata we need and populate the mappings.

src/EmojiGotchi.sol

function safeMint(address to) public {
    // Grab the current counter
    uint256 tokenId = _tokenIdCounter.current();
    // Increase it
    _tokenIdCounter.increment();
    // Mint the token
    _safeMint(to, tokenId);
    // Set the inital SVG to the first emoji value
    string memory finalSVG = string(abi.encodePacked(SVGBase, emojiBase64[0]));
    // Set attributes for the token to the default
    gotchiHolderAttributes[tokenId] = GotchiAttributes({
        gotchiIndex: tokenId,
        imageURI: finalSVG,
        happiness: 100,
        hunger: 100,
        enrichment: 100,
         lastChecked: block.timestamp
    });
    // Map the token to the holder/minter
    gotchiHolders[msg.sender] = tokenId;
    // Set the URI for the token to include all of the attributes
    _setTokenURI(tokenId, tokenURI(tokenId));

}

Once safeMint() is updated, you will need to update the output of the tokenURI() function to include the proper JSON based on our attributes. This is perhaps the most challenging portion of this contract. Not from a technical aspect, simply due to matching opening and closing quotes. The result will look something like this.

{
    "name": "Your Little Emoji Friend",
    "description": "Keep your pet happy!",
    "image": <image URI Here>,
    "traits": [
        {"trait_type": "Hunger", "value": "100"},
        {"trait_type": "Enrichment", "value": "100"},
        {"trait_type": "Happiness", "value": "100"}
    ]

}

Update the tokenURI() function to include the attributes and return a JSON object.

src/EmojiGotchi.sol

function tokenURI(uint256 _tokenId)
    public
    view
    override(ERC721, ERC721URIStorage)
    returns (string memory)
{

    // Select the attributes for the token we are referencing
    GotchiAttributes memory gotchiAttributes = gotchiHolderAttributes[_tokenId];

    // The result of this function is a string.
    // Each of these values, happiness, hunger, enrichment
    // is stored as a uint256, they need to be converted to strings
    string memory strHappiness = Strings.toString(gotchiAttributes.happiness);
    string memory strHunger = Strings.toString(gotchiAttributes.hunger);
    string memory strEnrichment = Strings.toString(gotchiAttributes.enrichment);

    // abi.encodePacked is used to combine the strings into a single value.
    string memory json = string(
      abi.encodePacked(
        '{"name": "Your Little Emoji Friend",',
        '"description": "Keep your pet happy!",',
        '"image": "',
        gotchiAttributes.imageURI,
        '",',
        '"traits": [',
        '{"trait_type": "Hunger","value": ',
        strHunger,
        '}, {"trait_type": "Enrichment", "value": ',
        strEnrichment,
        '}, {"trait_type": "Happiness","value": ',
        strHappiness,
        '}]',
        '}'
    )
  );
  return json;
}

Return the Stats for an Emojigotchi

Once you have created the mappings and updated the minting and token URI, you are ready to add the function we tested with testURI(). The test is expecting, in this order, happiness, hunger, enrichment, last checked, and the image. Each of these is stored in the mapping from token id to attributes gotchiHolderAttributes. You can use that to return the values based on the token id passed in.

src/EmojiGotchi.sol

function gotchiStats(uint256 _tokenId)
    public
    view
    returns (
        uint256,
        uint256,
        uint256,
        uint256,
        string memory
    )
{
    return (
        gotchiHolderAttributes[_tokenId].happiness,
        gotchiHolderAttributes[_tokenId].hunger,
        gotchiHolderAttributes[_tokenId].enrichment,
        gotchiHolderAttributes[_tokenId].lastChecked,
        gotchiHolderAttributes[_tokenId].imageURI
    );

}

Once you add in this function, your test should pass.

❯ foundry (main) ✘ forge test

[⠊] Compiling...
[⠰] Compiling 2 files with 0.8.13
[⠔] Solc finished in 294.37ms
Compiler run successful

Running 2 tests for src/test/EmojiGotchi.t.sol:EmojiGotchiTest
[PASS] testMint() (gas: 7844)
[PASS] testUri() (gas: 248121)
Test result: ok. 2 passed; 0 failed; finished in 2.61ms

❯ foundry (main) ✘

Returning Your EmojiGotchi

The next test will look similar to testUri(), with a minor difference. In testUri(), you passed in an id for the NFT. You will be modifying this to return the stats of the EmojiGotchi the msg.sender owns.

src/test/EmojiGotchi.t.sol

function testMyGotchi() public {
    (uint256 happiness, uint256 hunger, uint256 enrichment, uint256 checked, ) = eg.myGotchi() ;
    assertEq(happiness, (hunger + enrichment) / 2);
    assertEq(hunger, 100);
    assertEq(enrichment, 100);
    assertEq(checked, block.timestamp);

}

This test will fail until you add the myGotchi() function. This function will pass the msg.sender to the gotchiHolders mapping to return the id of the EmojiGotchi and retrieve its stats.

src/EmojiGotchi.sol

function myGotchi()
    public
    view
    returns (
        uint256,
        uint256,
        uint256,
        uint256,
        string memory
    )

{
    return gotchiStats(gotchiHolders[msg.sender]);
}

Enabling the Passage of Time

Now that you have a functional EmojiGotchi NFT, you will need to enable time to pass. In this case, that means you will need to create a function to decrease how well-fed and enriched your EmojiGotchi is. First, you should build the test.

src/test/EmojiGotchi.t.sol

function testPassTime() public {
    // Pass time for a specific NFT Id
    eg.passTime(0);
    // The empty value is the last time checked and image
    // which we aren't using in this test
    (uint256 happiness, uint256 hunger, uint256 enrichment, , ) = eg.gotchiStats(0);
    // passTime should reduce hunger and enrichment by 10
    assertEq(hunger, 90);
    assertEq(enrichment, 90);
    assertEq(happiness, (90 + 90) / 2);

}

Once the test is added, your tests should be failing again.

❯ foundry (main) ✘ forge test

[⠊] Compiling...
[⠢] Compiling 2 files with 0.8.13
[⠆] Solc finished in 19.76ms
Error: 
   0: Compiler run failed
      TypeError: Member "passTime" not found or not visible after argument-dependent lookup in contract EmojiGotchi.
        --> /Users/rg/Development/EmojiGotchi/foundry/src/test/EmojiGotchi.t.sol:52:9:
         |
      52 |         eg.passTime(0);
         |         ^^^^^^^^^^^    

   0: 

Location:
   cli/src/compile.rs:88

Backtrace omitted.
Run with RUST_BACKTRACE=1 environment variable to display it.
Run with RUST_BACKTRACE=full to include source snippets.

❯ foundry (main) ✘

You will need to decrease the hunger and enrichment value for the EmojiGotchi in the passTime() function. Once you have stored the hunger, enrichment, and happiness attributes, you will need to update the URI. This is where the emoji is changed. You will reference emojiBase64, which was set up earlier. It’s an array of the following emojis:

🤩, 😀, 😐, 😡, ☠

Thinking about the logic for these emojis, 🤩 & ☠ represent the maximum happiness, 100, and minimum, 0. The other three, 😀, 😐, & 😡 will be based on happiness above 66, 33, and 0, respectively.

src/EmojiGotchi.sol

function passTime(uint256 _tokenId) public {
    // decrease hunger
    gotchiHolderAttributes[_tokenId].hunger = gotchiHolderAttributes[_tokenId].hunger - 10;
    // decrease enrichment
    gotchiHolderAttributes[_tokenId].enrichment =
        gotchiHolderAttributes[_tokenId].enrichment -
        10;
    // recalculate happiness
    gotchiHolderAttributes[_tokenId].happiness =
        (gotchiHolderAttributes[_tokenId].hunger +
            gotchiHolderAttributes[_tokenId].enrichment) /
        2;
    // update the URI 
    updateURI(_tokenId)
}

function updateURI(uint256 _tokenId) private {
    // store the base case
    string memory emojiB64 = emojiBase64[0];
    // if happiness is 100: 🤩
    if (gotchiHolderAttributes[_tokenId].happiness == 100) {
        emojiB64 = emojiBase64[0];
    // if happiness is < 100 and > 66 😀
    } else if (gotchiHolderAttributes[_tokenId].happiness > 66) {
        emojiB64 = emojiBase64[1];
    // if happiness is <= 66 and > 33 😐
    } else if (gotchiHolderAttributes[_tokenId].happiness > 33) {
        emojiB64 = emojiBase64[2];
    // if happiness is between 33 and greater than 0 😡
    } else if (gotchiHolderAttributes[_tokenId].happiness > 0) {
        emojiB64 = emojiBase64[3];
    // if your emojigotchi is 'dead' happiness 0 ☠
    } else if (gotchiHolderAttributes[_tokenId].happiness == 0) {
        emojiB64 = emojiBase64[4];
    }
    // repack the SVG string with the emoji 
    string memory finalSVG = string(abi.encodePacked(SVGBase, emojiB64));
    //update the attributes for the token
    gotchiHolderAttributes[_tokenId].imageURI = finalSVG;
    // set the token URI to the new values
    _setTokenURI(_tokenId, tokenURI(_tokenId));

}

Once you add these functions, the tests should be back to passing.

❯ foundry (main) ✘ forge test

[⠊] Compiling...
[⠰] Compiling 2 files with 0.8.13
[⠒] Solc finished in 321.33ms
Compiler run successful

Running 4 tests for src/test/EmojiGotchi.t.sol:EmojiGotchiTest
[PASS] testMint() (gas: 7977)
[PASS] testMyGotchi() (gas: 250299)
[PASS] testPassTime() (gas: 806103)
[PASS] testUri() (gas: 248099)
Test result: ok. 4 passed; 0 failed; finished in 3.51ms

❯ foundry (main) ✘

Having Fun With Your EmojiGotchi

Now that you’ve created a way to cause your EmojiGotchi to be bored and hungry, you need to play with it and feed it. Again, start with the tests. These tests are very similar, so that we can write them simultaneously.

src/test/EmojiGotchi.t.sol

function testFeed() public {
    eg.passTime(0);
    eg.feed();
    (uint256 happiness, uint256 hunger, , , ) = eg.gotchiStats(0);
    assertEq(hunger, 100);
    assertEq(happiness, (100 + 90) / 2);
}

function testPlay() public {
    eg.passTime(0);
    eg.play();
    (uint256 happiness, , uint256 enrichment, , ) = eg.gotchiStats(0);
    assertEq(enrichment, 100);
    assertEq(happiness, (90 + 100) / 2);

}

To satisfy these tests you will need a feed() and play() function, which sets their respective attributes to 100 and recalculates happiness.

src/EmojiGotchi.sol

function feed() public {
    // retrieve the token based on the sender. 
    uint256 _tokenId = gotchiHolders[msg.sender];
    // update hunger
    gotchiHolderAttributes[_tokenId].hunger = 100;
    // recalculate happiness
    gotchiHolderAttributes[_tokenId].happiness =
      (gotchiHolderAttributes[_tokenId].hunger +
        gotchiHolderAttributes[_tokenId].enrichment) /
      2;
    // update the URI based on new attributes
    updateURI(_tokenId);
}

function play() public {
    // retrieve the token based on the sender. 
    uint256 _tokenId = gotchiHolders[msg.sender];
    // update enrichment
    gotchiHolderAttributes[_tokenId].enrichment = 100;
    // recalculate happiness
    gotchiHolderAttributes[_tokenId].happiness =
      (gotchiHolderAttributes[_tokenId].hunger +
        gotchiHolderAttributes[_tokenId].enrichment) /
     2;
     // update the URI based on new attributes
    updateURI(_tokenId);

}

Now you can play with and feed your EmojiGotchi, and your tests are back to passing.

❯ foundry (main) ✘ forge test

[⠊] Compiling...
[⠰] Compiling 2 files with 0.8.13
[⠒] Solc finished in 330.95ms
Compiler run successful

Running 6 tests for src/test/EmojiGotchi.t.sol:EmojiGotchiTest
[PASS] testFeed() (gas: 890794)
[PASS] testMint() (gas: 7999)
[PASS] testMyGotchi() (gas: 250321)
[PASS] testPassTime() (gas: 806125)
[PASS] testPlay() (gas: 893614)
[PASS] testUri() (gas: 248099)
Test result: ok. 6 passed; 0 failed; finished in 3.87ms

❯ foundry (main) ✘

Ensuring the Image URI Updates

When you implemented the updateURI() function, there wasn’t a test to ensure it was working. While this may be slightly backward in terms of test-drive development, you can fix that gap. 

You will be adding a helper function to compare strings and ensure they are not the same. In Solidity, comparing strings is best accomplished by encoding them into a hash. Keccak256 returns a bytes32 hash determined by the input. No matter what the input is, Keccak256 will return a 64-character value.

src/test/EmojiGotchi.t.sol

function compareStringsNot(string memory a, string memory b) public pure returns (bool) {
    return (keccak256(abi.encodePacked((a))) != keccak256(abi.encodePacked((b))));

}

Now, you can add a test to ensure that the image for the token is changing when it should.

src/test/EmojiGotchi.t.sol

function testImgURI() public {
    string memory tokenURI = '';
    (, , , , tokenURI) = eg.gotchiStats(0);
    string memory firstURI = tokenURI;
    eg.passTime(0);
    eg.passTime(0);
    eg.passTime(0);
    (, , , , tokenURI) = eg.gotchiStats(0);
    string memory secondURI = tokenURI;
    assertTrue(compareStringsNot(firstURI, secondURI));

}

Once both of these functions are added to the test contract, you should be back to passing. 

Testing Upkeep

At this point, you have created an EmojiGotchi that can experience time passing only when you manually pass time. Using Chainlink Keepers, you will be able to automate your contract, enabling time to pass at a regular interval. 

In order to test this, you are going to need to use a cheat code provided by Foundry. Before the contract is defined in src/test/EmojiGotchi.t.sol, add the following interface and instantiate it within the contract.

src/test/EmojiGotchi.t.sol

interface CheatCodes {
    function warp(uint256) external;
}

contract EmojiGotchiTest is DSTest {

    CheatCodes constant cheats = CheatCodes(HEVM_ADDRESS);

Then, within the EmojiGotchiTest contract, add another function to testUpkeep().

src/test/EmojiGotchi.t.sol

function testUpkeep() public {
    bytes memory data = '';
    bool upkeepNeeded = false;
    (upkeepNeeded, ) = eg.checkUpkeep(data);
    assertTrue(upkeepNeeded == false);
    cheats.warp(block.timestamp + 100);
    (upkeepNeeded, ) = eg.checkUpkeep(data);
    assertTrue(upkeepNeeded);

}

This test uses cheats.warp to warp forward in time, so we will need upkeep. The checkUpkeep() function will return a Boolean letting the Keepers network know if the contract needs an upkeep performed. 

To make our contract Keeper compatible, we require two functions. The first is checkUpkeep() which will return the Boolean mentioned above; the second is performUpkeep(), which actually makes the change to the blockchain. You can add both of those to your contract as follows.

src/EmojiGotchi.sol

function checkUpkeep(
    bytes calldata /* checkData */
)
    external
    view
    returns (
        bool upkeepNeeded,
        bytes memory /* performData */
    )
{
    // The last time the EmojiGotchi was updated
    uint256 lastTimeStamp = gotchiHolderAttributes[0].lastChecked;
    // If the EmojiGotchi's happiness is > 0 and
    // it's been more than 60s since last check 
    // return true
    // ** NOTE this example hard codes the first token
    // minted for this check. [0] 
    upkeepNeeded = (gotchiHolderAttributes[0].happiness > 0 &&
        (block.timestamp - lastTimeStamp) > 60);
    // We don't use the checkData in this example. The checkData is defined when the Upkeep was registered.
}

function performUpkeep(
    bytes calldata /* performData */
) external {
    uint256 lastTimeStamp = gotchiHolderAttributes[0].lastChecked;

    //We highly recommend revalidating the upkeep in the performUpkeep function
    // Run the same checks as checkUpkeep()
    if (
        gotchiHolderAttributes[0].happiness > 0 &&
        ((block.timestamp - lastTimeStamp) > 60)
    ) {
        // update the last checked value to now
        gotchiHolderAttributes[0].lastChecked = block.timestamp;
        // run passTime
        // ** NOTE this example hard codes the first token
        // minted for this check. [0] 
        passTime(0);
    }
    // We don't use the performData in this example. The performData is generated by the Keeper's call to your checkUpkeep function

}

With these additions, you have a fully functional, Keeper-compatible, on-chain NFT EmojiGotchi! Congratulations! 

There is one more addition to the contract you need to make: You need to add an event that you will emit every time the emoji is updated. At the top of your contract, add the event definition:

src/EmojiGotchi.sol

event EmojiUpdated(
    uint256 happiness,
    uint256 hunger,
    uint256 enrichment,
    uint256 checked,
    string uri,
    uint256 index

);

Then you will need to add a function to actually emit the update as well as add a call to the end of the updateURI() function. 

The new function:

src/EmojiGotchi.sol

function emitUpdate(uint256 _tokenId) internal {
    emit EmojiUpdated(
        gotchiHolderAttributes[_tokenId].happiness,
        gotchiHolderAttributes[_tokenId].hunger,
        gotchiHolderAttributes[_tokenId].enrichment,
        gotchiHolderAttributes[_tokenId].lastChecked,
        gotchiHolderAttributes[_tokenId].imageURI,
        _tokenId
    );

}

Change updateURI() to reflect the addition of this new event.

function updateURI(uint256 _tokenId) private {
    // store the base case
    string memory emojiB64 = emojiBase64[0];
    // if happiness is 100: 🤩
    if (gotchiHolderAttributes[_tokenId].happiness == 100) {
        emojiB64 = emojiBase64[0];
        // if happiness is < 100 and > 66 😀
    } else if (gotchiHolderAttributes[_tokenId].happiness > 66) {
        emojiB64 = emojiBase64[1];
        // if happiness is <= 66 and > 33 😐
    } else if (gotchiHolderAttributes[_tokenId].happiness > 33) {
        emojiB64 = emojiBase64[2];
        // if happiness is between 33 and greater than 0 😡
    } else if (gotchiHolderAttributes[_tokenId].happiness > 0) {
        emojiB64 = emojiBase64[3];
        // if your emojigotchi is 'dead' happiness 0 ☠
    } else if (gotchiHolderAttributes[_tokenId].happiness == 0) {
        emojiB64 = emojiBase64[4];
    }
    // repack the SVG string with the emoji
    string memory finalSVG = string(abi.encodePacked(SVGBase, emojiB64));
    //update the attributes for the token
    gotchiHolderAttributes[_tokenId].imageURI = finalSVG;
    // set the token URI to the new values
    _setTokenURI(_tokenId, tokenURI(_tokenId));
    emitUpdate(_tokenId);

}

Contract Completed!

You did it! The contract is ready to be deployed! You can use deploy.sh in the foundry directory to deploy your NFT contract. If you would like to mint one NFT to yourself on deploy, add the following to the constructor.

/src/EmojiGotchi.sol

constructor() ERC721("EmojiGotchi", "emg") {
    safeMint(msg.sender);

}

Once you run deploy.sh you should see where your contract was deployed to. You will need this address for the next section, in which we’ll build out the frontend.

Deployer: 0x0000000000000000000000000000000000000000
Deployed to: 0x1234567890123456789012345678901234567890

Transaction hash: 0x1234567890123456789012345678901234567890594be2f670606ada53412aaa

Building a SvelteKit Frontend

The second part of this tutorial will walk you through building out a SvelteKit-based frontend for the EmojiGotchi. This tutorial is focused on functionality and will leave the design choices up to you. 

SvelteKit Skeleton

The starter project begins with the SvelteKit Skeleton project and includes the addition of ethers. In order to get started you will need to install everything with the svelte directory

❯ svelte (main) ✔ npm install

> svelte@0.0.1 prepare
> svelte-kit sync

added 209 packages, and audited 210 packages in 1s

53 packages are looking for fundin
  run `npm fund` for details

found 0 vulnerabilities

❯ svelte (main) ✔

At this point you should be able to start the Svelte server and see the Welcome to SvelteKit page.

❯ svelte (main) ✔ npm run dev

> svelte@0.0.1 dev
> svelte-kit dev

  SvelteKit v1.0.0-next.325

  local:   http://localhost:3000
  network: not exposed

  Use --host to expose server to other devices on this network

Image showing Welcome to SvelteKit

SvelteKit will hot reload any changes you make to src/routes/index.svelte, if you change that file it should be reflected in your browser.

src/routes/index.svelte

<h1>My EmojiGotchi</h1>

This is where you can see your EmojiGotchi.

Image showing webpage titled My EmojiGotchi

With this up and running you can create your first component.

Connecting Your Wallet

You will need to create a component in the src/lib directory. To start simply and ensure everything is working just create a single button in the component for now.

src/lib/WalletConnect.svelte

<button>Attach Wallet</button>

Then within src/routes/index.svelte you can import this new component. Ensuring that the component is imported correctly is a great practice before fleshing it out completely. It also allows you to see incremental changes as you build the component out via hot reload.

src/routes/index.svelte

<script>
    import WalletConnect from '$lib/WalletConnect.svelte';
</script>

<h1>My EmojiGotchi</h1>

<WalletConnect />

This should provide you with the following change to your page.

Image of webpage showing where to attach wallet for My EmojiGotchi

Once this is working we can build out the rest of the components. I won’t be demonstrating these steps going forward but remember to create the component before importing them.

In order to pass the contract and wallet between components, you will need to create a place in which to store them. You can create a web3Props object that will hold this information.

src/lib/WalletConnect.svelte

<script>
    import { ethers } from 'ethers';
    // place holder for the properties we will be passing between components
    export let web3Props = { provider: null, signer: null, account: null, chainId: null };
    // connect the wallet
    async function connectWallet() {
        // get the provider, this time without ethereum object
        let provider = new ethers.providers.Web3Provider(window.ethereum, 'any');
        // prompt user for account connections
        await provider.send('eth_requestAccounts', []);
        // get the signer
        const signer = provider.getSigner();
        // get the account address
        const account = await signer.getAddress();
        // get the chainId
        const chainId = await signer.getChainId();
        // update the props
        web3Props = { signer, provider, chainId, account };
    }
</script>

<button on:click={connectWallet}>Attach Wallet</button>

Once the component is updated you will need to pass the props from index.svelte into the component.

src/routes/index.svelte

<script>
    import WalletConnect from '$lib/WalletConnect.svelte';

    export let web3Props = {
        provider: null,
        signer: null, 
        account: null,
        chainId: null
    };
</script>

<h1>My EmojiGotchi</h1>
{#if !web3Props.account}
    <WalletConnect bind:web3Props />
{:else}
    😎
{/if}

Gif showing how to use MetaMask with the app.

Adding Your Contract

Now that you have the ability to attach a wallet to your frontend you will need to import the contract information. The first step is to create a new directory, src/contracts, which will house the JSON ABI for your contract. If you head back to the foundry directory within foundry/out/EmojiGotchi.sol you will find EmojiGotchi.json. Copy this file into src/contracts within the svelte portion of the project.

This file houses the ABI for your contract. You will need to import it and add the information to the web3Props as well as the WalletConnect component. Additionally, you will need to add a constant for the contract address. This is the value from deploying your contract above.

src/routes/index.svelte

<script>
    // new import
    import EmojiGotchiAbi from '../contracts/EmojiGotchi.json';
    // new constant for contract address
    const contractAddr="<PLACE HOLDER>";

    export let web3Props = {
        provider: null,
        signer: null,
        account: null,
        chainId: null,
        // new prop
        contract: null
    };
</script>
{#if !web3Props.account}
    // new values passed to component
    <WalletConnect bind:web3Props {contractAddr} contractAbi={EmojiGotchiAbi} />
{:else}
    😎
{/if}
src/lib/WalletConnect.svelte

<script>
    import { ethers } from 'ethers';
    export let web3Props = {
        provider: null,
        signer: null,
        account: null, 
        chainId: null,
        // new prop
        contract: null
    };
    // new variable for the contract address
    export let contractAddr="";
    // new variable for the contract ABI
    export let contractAbi = { abi: null };

    async function connectWallet() {
        let provider = new ethers.providers.Web3Provider(window.ethereum, 'any');
        await provider.send('eth_requestAccounts', []);
        const signer = provider.getSigner();
        const account = await signer.getAddress();
        const chainId = await signer.getChainId();
        // new contract variable
        const contract = new ethers.Contract(contractAddr, contractAbi.abi, signer);
        // new value for contract
        web3Props = { signer, provider, chainId, account, contract };
    }
</script>

<button on:click={connectWallet}>Attach Wallet</button>

Fantastic! At this point, you have your wallet connected to the blockchain and you have all of the contract information ready to go. Let’s build out the EmojiGotchi interface.

Building The EmojiGotchi Component

Taking a look at the end result we are working towards we need to build a few things, the EmojiGotchi image, progress bars, the values, and a couple of buttons.

Gif showing My EmojiGotchi app

First, you’ll be building out a few sub-components. You will start by building the face of your EmojiGotchi.

src/lib/Face.svelte

<script>
    export let image;
</script>

<img src={image} alt="Your EmojiGotchi" />

This is a very simple component, it takes in an image, in this case an SVG which is base64 encoded, and displays it. 

Next, you can build out the progress bar component to show how much happiness or enrichment is left.

src/lib/Bar.svelte

<script>
    export let status;
</script>

<div class="status" style={`width: ${status}%;`} />

<style>
    .status {
        height: 8px;
        background: blue;
        width: 33%;
    }

</style>

With these two components in place, you are able to build out the full EmojiGotchi component.

src/lib/EmojiGotchi.svelte

<script>
    import Bar from './Bar.svelte';
    import Face from './Face.svelte';
    export let web3Props = {
        provider: null,
        signer: null,
        account: null,
        chainId: null,
        contract: null
    };
    // set placeholders for variables
    // $: in svelte denotes a reactive declaration
    // it will be updated when the value changes
    $: image="";
    $: hunger = 0;
    $: enrichment = 0;
    $: happiness = 0;
    const getMyGotchi = async () => {
        // get the contract instance
        let currentGotchi = await web3Props.contract.myGotchi();
        // get the current gotchi's image
        image = await currentGotchi[4];
        // get the current gotchi's happiness
        happiness = await currentGotchi[0].toNumber();
        // get the current gotchi's hunger
        hunger = await currentGotchi[1].toNumber();
        // get the current gotchi's enrichment
        enrichment = await currentGotchi[2].toNumber();
        // Listen for the EmojiUpdated event and update the values
        web3Props.contract.on('EmojiUpdated', async () => {
             currentGotchi = await web3Props.contract.myGotchi();
             image = await currentGotchi[4];
             happiness = await currentGotchi[0].toNumber();
             hunger = await currentGotchi[1].toNumber();
             enrichment = await currentGotchi[2].toNumber();
        });
    };
    getMyGotchi();
</script>

<div>
    <Face {image} />
</div>
<div>
    Hunger: {hunger}
    <br />
    <Bar bind:status={hunger} />
    <button
        on:click={() => {
            web3Props.contract.feed();
        }}>Feed</button
    >
</div>
<div>
    Enrichment: {enrichment}
    <br />
    <Bar bind:status={enrichment} />
    <button
        on:click={() => {
            web3Props.contract.play();
        }}>Play</button
    >
</div>
<div>
    Happiness: {happiness}
    <Bar bind:status={happiness} />
</div>

<style>
    div {
        width: 33%;
    }

</style>

Connect the EmojiGotchi Component

If you haven’t already, the last step is to add this component to index.svelte.

<script>
    import EmojiGotchiAbi from '../contracts/EmojiGotchi.json';
    import WalletConnect from '$lib/WalletConnect.svelte';
    import EmojiGotchi from '../lib/EmojiGotchi.svelte';

    const contractAddr="<PLACE HOLDER>";
    export let web3Props = {
        provider: null,
        signer: null,
        account: null,
        chainId: null,
        contract: null
    };
</script>

<h1>My EmojiGotchi</h1>
{#if !web3Props.account}
    <WalletConnect bind:web3Props {contractAddr} contractAbi={EmojiGotchiAbi} />
{:else}
    <EmojiGotchi bind:web3Props />
{/if}

Make It Dynamic

Now that you have a fully complete dApp, there is one final step. You need to head to keepers.chain.link and register a new Upkeep. Fill in the relevant information and submit your upkeep. As you watch your EmojiGotchi, you will see its stats decreasing. Make sure you keep it happy!

Learn more about Chainlink by visiting chain.link or reading the documentation at docs.chain.link. To discuss an integration, reach out to an expert.

The post How To Create an NFT Game appeared first on Chainlink Blog.

>> View on Chainlink

Join us on Telegram

Follow us on Twitter

Follow us on Facebook

You might also like

LATEST NEWS

LASTEST NEWS