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
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 🐋
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
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 :
parameter
. It can be whatever type but it has to be an unique variable as argumentunit
by default)operation
and a storageNote : The old syntax was requiring a main function. It is still valid but very verbatim
Poke
will be generated frompoke
entrypoint and will generate avariant
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), }); };
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 :
Here, we get the caller address using Tezos.get_source()
. Tezos library provides useful function for manipulating blockchain objects
The LIGO command-line interpreter provides sub-commands to directly test your LIGO code
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
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
⚠️ 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
Set alice as taqueria operator
Edit .taq/config.local.testing.json
{
"networkName": "ghostnet",
"accounts": {
"taqOperatorAccount": {
"publicKey": "edpkvGfYw3LyB1UcCahKQk4rF2tvbMUk8GFiTuMjL75uGXrpvKXhjn",
"publicKeyHash": "tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb",
"privateKey": "edsk3QoqBuvdamxouPhin7swCvkQNgq4jP5KZPbwWNnwdZpSpJiEbq"
}
}
}
Look at .taq/config.local.testing.json file to get the privateKey
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
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 💰 !!!
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 │
└─────────────┴──────────────────────────────────────┴──────────┴──────────────────┴────────────────────────────────┘
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
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
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> 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> 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)
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
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
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 uniquedefault
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
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
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 ...