📍 See Github version and full code here

Training dapp n°1

☝️ Poke game

dapp : A decentralized application (dApp) is a type of distributed open source software application that runs on a peer-to-peer (P2P) blockchain network rather than on a single computer. DApps are visibly similar to other software applications that are supported on a website or mobile device but are P2P supported

We are creating a poke game on smart contract. You will learn :

⚠️ This is not an HTML or REACT training, I will avoid as much of possible any complexity relative to these technologies

The game consists on poking the owner of a smart contract. The smartcontract keeps a track of user interactions and stores this trace.

Poke sequence diagram

sequenceDiagram
  Note left of User: Prepare poke
  User->>SM: poke owner
  Note right of SM: store poke trace

📝 Prerequisites

Please install this software first on your machine or use online alternative :

⚠️ 🐳 About Taqueria : taqueria is using software images from Docker to run Ligo, etc ... Docker should be running on your machine 🐋

📜 Smart contract

Step 1 : Create folder & file

Note : We will use CLI here but you can also use GUI from the IDE or Taqueria plugin

taq init training1
cd training1
taq install @taqueria/plugin-ligo
taq create contract pokeGame.jsligo

Step 2 : Edit pokeGame.jsligo

Remove the default code and paste this code instead

type storage = unit;
type parameter = unit;

type return_ = [list<operation>, storage];

//@entry
const poke = ( _ : parameter , store: storage): return_ => {
  return [list([]) as list<operation>, store];
};

Every contract requires to respect this convention :

Click here to see the Entrypoints contracts documentation>

Note : The old syntax was requiring a main function. It is still valid but very verbatim

Poke will be generated from poke entrypoint and will generate a variant type under the hood. It is a bit equivalent of Enum type in javascript. For each entries, a variant case will be added to the global parameter, here is kinda what intermediate state will produce (also a default main function will be generated too, you don't have to think about it anymore) :

type all_parameters = |["Poke"];

...

const main = ([action, store]: [all_parameters, storage]): return_ => {
 return match(action, {
   Poke: (args) => poke(args,store),
 });
};

Click here to understand the variant type

Step 3 : Write the poke function

We want to store every caller address poking the contract. Let's redefine storage, and then add the caller to the set of poke guys

At line 1, replace :

type storage = set<address>;

change poke function to :

//@entry
const poke = ( _ : parameter , store: storage): return_ => {
  return [list([]) as list<operation>, Set.add(Tezos.get_source(), store)];
};

Set library has specific usage :

Click here to see Set library documentation

Here, we get the caller address using Tezos.get_source(). Tezos library provides useful function for manipulating blockchain objects

Click here to see Tezos library documentation

Step 4 : Try to poke

The LIGO command-line interpreter provides sub-commands to directly test your LIGO code

Click here to see Testing documentation

Compile contract (to check any error, and prepare the michelson outputfile to deploy later) :

TAQ_LIGO_IMAGE=ligolang/ligo:0.65.0 taq compile pokeGame.jsligo

Taqueria is creating the Michelson file output on artifacts folder

To compile an initial storage with taqueria, edit the new file pokeGame.storageList.jsligo

Replace current code by

#include "pokeGame.jsligo"
const default_storage = Set.empty as set<address>;

Compile all now

TAQ_LIGO_IMAGE=ligolang/ligo:0.65.0 taq compile pokeGame.jsligo

It compiles both source code and storage now. (You can also pass an argument -e to change the environment target for your storage initialization)

Let's simulate the Poke call using taq simulate
We will pass the contract parameter unit and the initial on-chain storage with an empty set

Edit the new file pokeGame.parameterList.jsligo

#include "pokeGame.jsligo"
const default_parameter : parameter = unit;

Run simulation now (you will need tezos client plugin for simulation)

taq install @taqueria/plugin-tezos-client
TAQ_LIGO_IMAGE=ligolang/ligo:0.65.0 taq compile pokeGame.jsligo
taq simulate pokeGame.tz --param pokeGame.parameter.default_parameter.tz

Output should give :

┌─────────────┬──────────────────────────────────────────────┐
│ Contract    │ Result                                       │
├─────────────┼──────────────────────────────────────────────┤
│ pokeGame.tz │ storage                                      │
│             │   { "tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU" } │
│             │ emitted operations                           │
│             │                                              │
│             │ big_map diff                                 │
│             │                                              │
│             │                                              │
└─────────────┴──────────────────────────────────────────────┘

You can notice that the instruction will store the address of the caller into the traces storage

Step 5 : Configure your wallet to get free Tez

Local testnet wallet

Flextesa local testnet includes already some accounts with XTZ (alice,bob,...), so you don't really need to configure something. Anyway, we will not use local testnet and deploy directly to the ghostnet

Ghostnet testnet wallet

⚠️ Taqueria will require an account (mainly to deploy your contract), the first time you will try to deploy a contract it will generate a new implicit account you will have to fill with XTZ

Force Taqueria to generate this account

taq install @taqueria/plugin-taquito
taq deploy pokeGame.tz -e "testing"

You should get this kind of log

Warning: the faucet field in network configs has been deprecated and will be ignored
A keypair with public key hash tz1XXXXXXXXXXXXXXXXXXXXXX was generated for you.
To fund this account:
1. Go to https://teztnets.xyz and click "Faucet" of the target testnet
2. Copy and paste the above key into the 'wallet address field
3. Request some Tez (Note that you might need to wait for a few seconds for the network to register the funds)
No operations performed

Choice 1 : use alice wallet everywhere

Set alice as taqueria operator

Edit .taq/config.local.testing.json

{
  "networkName": "ghostnet",
  "accounts": {
    "taqOperatorAccount": {
      "publicKey": "edpkvGfYw3LyB1UcCahKQk4rF2tvbMUk8GFiTuMjL75uGXrpvKXhjn",
      "publicKeyHash": "tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb",
      "privateKey": "edsk3QoqBuvdamxouPhin7swCvkQNgq4jP5KZPbwWNnwdZpSpJiEbq"
    }
  }
}

Choice 2 : use the Taqueria generated account instead

Look at .taq/config.local.testing.json file to get the privateKey

Configure Temple

Open your Temple browser extension or on your mobile phone. Do the initial setup. Once you are done, go to Settings (click on the avatar icon, or display Temple in full page) and click on Import account > Private key tab

Copy the privateKey from alice (or generated taqueria account) and paste it to Temple text input

Send free XTZ to your account

If you don't have enough XTZ to start, go to a web faucet like Marigold faucet here. Connect your wallet on Ghostnet and ask for free XTZ

Now you have 💰 !!!

Step 6 : Deploy to Ghostnet testnet

Retry to deploy to testing env

taq deploy pokeGame.tz -e "testing"

HOORAY 🎊 your smart contract is ready on the Ghostnet !

┌─────────────┬──────────────────────────────────────┬──────────┬──────────────────┬────────────────────────────────┐
│ Contract    │ Address                              │ Alias    │ Balance In Mutez │ Destination                    │
├─────────────┼──────────────────────────────────────┼──────────┼──────────────────┼────────────────────────────────┤
│ pokeGame.tz │ KT1SVwMCVR9T3nq1sRsULy2uYaBRG1nqT9rz │ pokeGame │ 0                │ https://ghostnet.ecadinfra.com │
└─────────────┴──────────────────────────────────────┴──────────┴──────────────────┴────────────────────────────────┘

👷 Dapp

Step 1 : Create react app

yarn create react-app app --template typescript

cd app

Add taquito, tzkt indexer lib

yarn add @taquito/taquito @taquito/beacon-wallet @airgap/beacon-sdk  @dipdup/tzkt-api
yarn add -D @airgap/beacon-types

⚠️ ⚠️ ⚠️ Last React version uses react-script 5.x , follow these steps to rewire webpack for all encountered missing libraries : https://github.com/ChainSafe/web3.js#troubleshooting-and-known-issues

For example, in my case, I installed this :

yarn add --dev react-app-rewired process crypto-browserify stream-browserify assert stream-http https-browserify os-browserify url path-browserify

and my config-overrides.js file was :

const webpack = require("webpack");

module.exports = function override(config) {
  const fallback = config.resolve.fallback || {};
  Object.assign(fallback, {
    crypto: require.resolve("crypto-browserify"),
    stream: require.resolve("stream-browserify"),
    assert: require.resolve("assert"),
    http: require.resolve("stream-http"),
    https: require.resolve("https-browserify"),
    os: require.resolve("os-browserify"),
    url: require.resolve("url"),
    path: require.resolve("path-browserify"),
  });
  config.ignoreWarnings = [/Failed to parse source map/];
  config.resolve.fallback = fallback;
  config.plugins = (config.plugins || []).concat([
    new webpack.ProvidePlugin({
      process: "process/browser",
      Buffer: ["buffer", "Buffer"],
    }),
  ]);
  return config;
};

then I change the script in package.json by

"scripts": {
   "start": "react-app-rewired start",
   "build": "react-app-rewired build",
   "test": "react-app-rewired test",
   "eject": "react-app-rewired eject"
},

⚠️

This was painful 😕, but it was the worst so far

Generate Typescript classes from Michelson code

Taqueria is able to generate Typescript classes for our React application. It will take the definition of your smart contract and generate the contract entrypoint functions, type definitions, etc ...

To get typescript classes from taqueria plugin, get back to root folder running :

cd ..

taq install @taqueria/plugin-contract-types

taq generate types ./app/src

cd ./app

Start the dev server

yarn run start

Open your browser at : http://localhost:3000/ Your app should be running

Step 2 : Connect / disconnect the wallet

We will declare 2 React Button components and a display of address and balance while connected

Edit src/App.tsx file

import { NetworkType } from "@airgap/beacon-types";
import { BeaconWallet } from "@taquito/beacon-wallet";
import { TezosToolkit } from "@taquito/taquito";
import { useEffect, useState } from "react";
import "./App.css";
import ConnectButton from "./ConnectWallet";
import DisconnectButton from "./DisconnectWallet";

function App() {
  const [Tezos, setTezos] = useState<TezosToolkit>(
    new TezosToolkit("https://ghostnet.tezos.marigold.dev")
  );
  const [wallet, setWallet] = useState<BeaconWallet>(
    new BeaconWallet({
      name: "Training",
      preferredNetwork: NetworkType.GHOSTNET,
    })
  );

  useEffect(() => {
    Tezos.setWalletProvider(wallet);
    (async () => {
      const activeAccount = await wallet.client.getActiveAccount();
      if (activeAccount) {
        setUserAddress(activeAccount.address);
        const balance = await Tezos.tz.getBalance(activeAccount.address);
        setUserBalance(balance.toNumber());
      }
    })();
  }, [wallet]);

  const [userAddress, setUserAddress] = useState<string>("");
  const [userBalance, setUserBalance] = useState<number>(0);

  return (
    <div className="App">
      <header className="App-header">
        <ConnectButton
          Tezos={Tezos}
          setUserAddress={setUserAddress}
          setUserBalance={setUserBalance}
          wallet={wallet}
        />

        <DisconnectButton
          wallet={wallet}
          setUserAddress={setUserAddress}
          setUserBalance={setUserBalance}
        />

        <div>
          I am {userAddress} with {userBalance} mutez
        </div>
      </header>
    </div>
  );
}

export default App;

Let's create the 2 missing src component files and put code in it. On src folder, create these files.

touch src/ConnectWallet.tsx
touch src/DisconnectWallet.tsx

ConnectWallet button will create an instance wallet, get user permissions via a popup and then retrieve account information

Edit ConnectWallet.tsx

import { NetworkType } from "@airgap/beacon-sdk";
import { BeaconWallet } from "@taquito/beacon-wallet";
import { TezosToolkit } from "@taquito/taquito";
import { Dispatch, SetStateAction } from "react";

type ButtonProps = {
  Tezos: TezosToolkit;
  setUserAddress: Dispatch<SetStateAction<string>>;
  setUserBalance: Dispatch<SetStateAction<number>>;
  wallet: BeaconWallet;
};

const ConnectButton = ({
  Tezos,
  setUserAddress,
  setUserBalance,
  wallet,
}: ButtonProps): JSX.Element => {
  const connectWallet = async (): Promise<void> => {
    try {
      await wallet.requestPermissions({
        network: {
          type: NetworkType.GHOSTNET,
          rpcUrl: "https://ghostnet.tezos.marigold.dev",
        },
      });
      // gets user's address
      const userAddress = await wallet.getPKH();
      const balance = await Tezos.tz.getBalance(userAddress);
      setUserBalance(balance.toNumber());
      setUserAddress(userAddress);
    } catch (error) {
      console.log(error);
    }
  };

  return (
    <div className="buttons">
      <button className="button" onClick={connectWallet}>
        <span>
          <i className="fas fa-wallet"></i>&nbsp; Connect with wallet
        </span>
      </button>
    </div>
  );
};

export default ConnectButton;

DisconnectWallet button will clean wallet instance and all linked objects

import { BeaconWallet } from "@taquito/beacon-wallet";
import { Dispatch, SetStateAction } from "react";

interface ButtonProps {
  wallet: BeaconWallet;
  setUserAddress: Dispatch<SetStateAction<string>>;
  setUserBalance: Dispatch<SetStateAction<number>>;
}

const DisconnectButton = ({
  wallet,
  setUserAddress,
  setUserBalance,
}: ButtonProps): JSX.Element => {
  const disconnectWallet = async (): Promise<void> => {
    setUserAddress("");
    setUserBalance(0);
    console.log("disconnecting wallet");
    await wallet.clearActiveAccount();
  };

  return (
    <div className="buttons">
      <button className="button" onClick={disconnectWallet}>
        <i className="fas fa-times"></i>&nbsp; Disconnect wallet
      </button>
    </div>
  );
};

export default DisconnectButton;

Save both file, the dev server should refresh the page

As Temple is configured well, Click on Connect button

On the popup, select your Temple wallet, then your account and connect.

🎊 your are "logged"

Click on the Disconnect button (if you want to test it too)

Step 3 : List poke contracts via an indexer

Remember that you deployed your contract previously. Instead of querying heavily the rpc node to search where is located your contract and get back some information about it, we can use an indexer. We can consider it as an enriched cache API on top of rpc node. In this example, we will use the tzkt indexer

Install jq

On package.json, change the start script line, prefixing with jq command to create an new env var pointing to your last smart contract address on testing env :

    "start": "jq -r '\"REACT_APP_CONTRACT_ADDRESS=\" + last(.tasks[]).output[0].address' ../.taq/testing-state.json > .env && react-app-rewired start",

You are pointing now to the last contract deployed on Ghostnet by taqueria

We will add a button to fetch all similar contracts to the one you deployed, then we display the list

Now, edit App.tsx to add 1 import on top of the file

import { Contract, ContractsService } from "@dipdup/tzkt-api";

Before the return , add this section for the fetch

const contractsService = new ContractsService({
  baseUrl: "https://api.ghostnet.tzkt.io",
  version: "",
  withCredentials: false,
});
const [contracts, setContracts] = useState<Array<Contract>>([]);

const fetchContracts = () => {
  (async () => {
    setContracts(
      await contractsService.getSimilar({
        address: process.env["REACT_APP_CONTRACT_ADDRESS"]!,
        includeStorage: true,
        sort: { desc: "id" },
      })
    );
  })();
};

On the return 'html templating' section, add this after the display of the user balance div I am {userAddress} with {userBalance} mutez, add this :

<br />
<div>
  <button onClick={fetchContracts}>Fetch contracts</button>
  {contracts.map((contract) =>
  <div>{contract.address}</div>
  )}
</div>

Save your file, and re-run your server , it will generate the .env file containing the last deployed contracts 😃

yarn run start

Go to the browser. click on Fetch contracts button

🎊 Congrats ! you are able to list all similar deployed contracts

Step 4 : Poke your contract

Add this import and this new function inside the App function, it will call the entrypoint to poke

import { PokeGameWalletType } from "./pokeGame.types";
const poke = async (contract: Contract) => {
  let c: PokeGameWalletType = await Tezos.wallet.at<PokeGameWalletType>(
    "" + contract.address
  );
  try {
    const op = await c.methods.default().send();
    await op.confirmation();
    alert("Tx done");
  } catch (error: any) {
    console.table(`Error: ${JSON.stringify(error, null, 2)}`);
  }
};

⚠️ Normally we should call c.methods.poke() function , but with a unique entrypoint, Michelson is required a unique default name instead of having the name of the function. Also be careful because all entrypoints naming are converting to lowercase whatever variant variable name you can have on source file.

Then replace the line displaying the contract address {contracts.map((contract) => <div>{contract.address}</div>)} by this one that will add a Poke button

    {contracts.map((contract) => <div>{contract.address} <button onClick={() =>poke(contract)}>Poke</button></div>)}

There is a Taqueria bug on unique entrypoint: https://github.com/ecadlabs/taqueria/issues/1128 Go to ./app/src/pokeGame.types.ts and rewrite these lines

type Methods = {
  default: () => Promise<void>;
};

type MethodsObject = {
  default: () => Promise<void>;
};

Save and see the page refreshed, then click on Poke button

🎊 If you have enough Tz on your wallet for the gas, then it should have successfully call the contract and added you to the list of poke guyz

Step 5 : Display poke guys

To verify that on the page, we can display the list of poke guyz directly on the page

Replace again the html contracts line {contracts ...} by this one

<table><thead><tr><th>address</th><th>people</th><th>action</th></tr></thead><tbody>
    {contracts.map((contract) => <tr><td style={{borderStyle: "dotted"}}>{contract.address}</td><td style={{borderStyle: "dotted"}}>{contract.storage.join(", ")}</td><td style={{borderStyle: "dotted"}}><button onClick={() =>poke(contract)}>Poke</button></td></tr>)}
    </tbody></table>

Contracts are displaying its people now

ℹ️ Wait around few second for blockchain confirmation and click on fetch contracts to refresh the list

🎊 Congratulation, you have completed this first dapp training

🌴 Conclusion 🌞

Now, you are able to create any Smart Contract using Ligo and create a complete Dapp via Taqueria/Taquito.

On next training, you will learn how to call a Smart contract inside a Smart Contract and use the callback, write unit test, etc ...

➡️ NEXT (HTML version)

➡️ NEXT (Github version)