Introduction to building a dApp

Learn how to build a simple dApp on Tezos using React and Archetype.

Written by

Introduction

This section presents how to build a dApp (Decentralized Application) with Archetype and Completium CLI.

Definition

Compared to a standard application, a dApp uses decentralized (peer to peer) backends (that is not owned by any private or public entity):

  1. a blockchain for its business logic (provided by a smart contract running on it)
  2. IPFS (Interplanetary File System) for storage
  3. ...

The user is authenticated on the blockchain with a wallet, whose main role is to sign transactions to the smart contract. The signature principle is the one of asymmetric cryptography: the wallet uses the user's private key to sign, while the public key (or its hash in Tezos case) is used as public address (identity) on the blockchain.

Limits

Many elements of a dApp are centralized.

User interface

The user interface is usually a web page (or a native app) served by a centralized server (owned by an ISP, Github, Gitlab, ...).

The UI code source should be publically available, so that every one can run it locally (the same applies to the wallet).

Endpoints

Accessing to a peer to peer network (blockchain, IPFS, ...) is done by connecting to one machine, called the endpoint, owned by one entity. The integrity of the dApp then depends on that entity.

The dApp should provide the possiblity to set endpoints' URL/IP addresses.

The user could potentially use its own endpoints.

Indexers

Indexers provide key informations to the ecosystem; however, as a centralised point of information access, it is recommended to use them only when necessary, that is when the required information is not available in any contract's storage or in any current block.

For example, when the dApp is required to react to contracts' events, it is recommended to use an embedded block explorer, such as @completium/event-listener

DApp UI

This section presents how to create and setup a dApp's User Interface (UI) project using React and Beacon's dApp/wallet interaction.

Technical stack

JS Runtime

The use of Nodejs as the javascript execution environment is a no brainer. It is best installed with nvm.

UI Framework

Any UI framework is suitable to create a web dApp interface (Angular, Vue, Svelte, ...). We present here how to create a dApp UI project with React.

Language

It is strongly recommended to use Typescript language as it will greatly shorten and ease up the development cycle. Its typed aspect makes that many errors are detected at compilation time (ie. at the time of writing code) rather than later at execution. It just doesn't make sense to developp with untyped languages when you think of it ...

Project templates configured with typescript are available with most UI frameworks. The following command uses create-react-app template with typescript:

npx create-react-app my-dapp --template typescript

This creates the my-dapp project. More information may be found here

Tezos library

While it is always possible to interact directly with the Tezos endpoint's RPC API, it is more than recommended to use a dedicated library that will wrap all services in high-level development services and take care of all the low-level Tezos protocol details.

In the context of web dApps for Tezos, the main library is Taquito:

npm install @taquito/taquito

Taquito uses official cryptographic packages (aka libraries). Some of these packages relies on nodejs packages designed to run on the back-end side (server side, not in a browser). These packages are crypto, stream, assert, http, https, os. As a result, the default build process fails; it is then necessary to map these packages to their front-end counterparts in the build process:

Follow instructions available here to solve build issues.

As a comment, create-react-app uses webpack (version 5) to bundle all resources as static web assets. It is then necessary to use the react-app-rewired package as described in instructions above, to be able to provide a customized webpack configuration without ejecting the react app.

Wallets

A dApp needs to interact with a wallet to sign operations (transfers, calls to a smart contract, ...). Many wallets are available on Tezos (Temple, Kukai, Umami, ...). It is common practice to interact with them all via Beaconthat implements the interaction standard TZIP-10between a wallet and a dApp, as it greatly reduces the integration effort with wallets.

npm install @taquito/beacon-wallet @airgap/beacon-sdk

The main drawback of Beacon is the lack of control over the UI elements (typically the wallet selection popup), which can be a no-go if you want a tight control of the dApp L&F. In that case, each wallet needs to be integrated separately.

A plug-and-play constate context for Beacon services connect and disconnect is available here.

Contracts bindings

When interacting with a contract (read and write), it is strongly recommended to use its generated typescript bindings, that is a typed high-level typescript interface. It greatly reduces the effort to call a contract, read its storage and the number of runtime errors, as the compilier and LSP guides you through the contract interface.

Typescript bindings may be obtained with the following Completium CLI command:

completium-cli generate binding-dapp-ts mycontract.arl > mycontract.ts
Bindings generation is also available for Michelson (.tz) files.

The generated binding interface relies on two packages:

npm install @completium/dapp-ts @completium/archetype-ts-types

Contexts

With React applications, it is strongly recommended to setup contexts for application data (settings, UI states, ...) in read/write modes with a dedicated package like constate or redux. This is preventing from awkward spaghetti code of passing components states and data through large hierarchy of components.

The dApp example presented here is using constate for its lightweight aspect.

npm install constate

Taquito and Beacon must be singletons, hence there are wrapped as contexts (with constate) to make them available to UI components. The same stands for the contract bindings.

The main 4 blockchain-related contexts are provided as plug-and-play code:

Contexts

Blockchain-related dApp settings
Provides Taquito's Tezos Toolkit hook
Beacon's connect connect and wallet address services
Provides contract binder

Architecture

Schema below illustrates the module and package architecture of the dApp and their interactions:

  1. Changes in Contexts data automatically redraw UI components that use them (React + constate hook mecanism)
  2. Contexts and UI interact with contract via Binding
  3. Contexts uses Beacon's services to connect to a wallet
  4. Taquito's Tezos toolkit uses Beacon as transaction signer
  5. Binding uses @completium/dapp-ts package services to interact with blockchain
  6. @completium/dapp-ts uses @completium/event-listener to listen to emitted events

File structure

├── README.md
├── config-overrides.js
├── package.json
├── tsconfig.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── manifest.json
│   └── robots.txt
└── src
    ├── App.css
    ├── App.test.tsx
    ├── App.tsx
    ├── index.css
    ├── index.tsx
    ├── react-app-env.d.ts
    ├── reportWebVitals.ts
    ├── setupTests.ts
    ├── bindings
    │   └── ...
    ├── components
    │   └── ...
    ├── routes
    │   └── ...
    └── contexts
        ├── Beacon.tsx
        ├── Taquito.tsx
        ├── Contract.tsx
        ├── Settings.tsx
        └── ...

Note that 4 directories are created under src:

  • 1. bindings: generated contracts bindings
  • 2. routes: page UI components managed by a route manager (for example react-router-dom)
  • 3. components: other UI components
  • 4. contexts: hooks providers

Settings.tsx

└── src
    └── store
        ├── Settings.tsx

The Settings context provides components (and other contexts) with access to the main blockchain settings required by Taquito and Beacon:

  1. endpoint URL used by Taquito's constructor
  2. network type used by beacon constructor

Code

"import { NetworkType } from "@airgap/beacon-sdk";
import constate from "constate";
import { useState } from 'react';

export const [
  SettingsProvider,
  useAppName,
  useEndpoint,
  useNetwork,
  useContractAddress
] = constate(
  () => {
    const [settingState] = useState({
      app_name        : 'My DApp',
      endpoint        : 'https://ghostnet.ecadinfra.com',
      network         :  NetworkType.GHOSTNET,
      contract        : 'KT1...',
    });
    return settings;
  },
  v => v.app_name,
  v => v.endpoint,
  v => v.network,
  v => v.contract
);"

Usage

When a component needs a settings, it imports the hook and calls it to retrieve the value:

import { useAppName, useEndpoint, useNetwork } from '../store/Settings'

const network = useNetwork()
const endpoint = useEndpoint()
const name = useAppName()

See the Beacon.tsx constate hook for an example.

Taquito.tsx

└── src
    └── store
        ├── Taquito.tsx

The Taquito context provides Taquito's Tezos Toolkit to all components. It is created once at context creation.

Code

The code below can be copied/pasted in your project as is (constate can coexist with redux)

Usage

When a component needs to use Taquito's Tezos Toolkit, it retrieves it with the useTezosToolkit hook:

import { useTezosToolkit } from '../store/Taquito'

const ttk = useTezosToolkit()
const balance = await ttk.tz.getBalance('tz1h4CiqWxNe4UxSpkwXy617RM6DaK6NU76P');

Beacon.tsx

└── src
    └── store
        ├── Beacon.tsx

The Beacon context provides the following services:

Beacon.tsx

useWalletAddress
Wallet address (or undefined if not logged in)
useWalletName
Wallet name (or undefined if not logged in)
useIsConnected
Function that returns true if user is logged in, false otherwise
useConnect
Beacon's connect service
useDisconnect
Beacon's disconnect service

Code

The code below can be copied/pasted in your project as is (constate can coexist with redux)
import { BeaconWallet } from '@taquito/beacon-wallet';
import constate from 'constate';
import React from 'react';

import { useAppName, useEndpoint, useNetwork } from "./Settings";
import { useTezosToolkit } from "./Taquito";

export const [
  BeaconProvider,
  useWalletAddress,
  useWalletName,
  useConnect,
  useDisconnect,
  useIsConnected
] = constate(
  MakeBeacon,
  (v) => v.beaconState.user_address,
  (v) => v.beaconState.wallet,
  (v) => v.utils.connect,
  (v) => v.utils.disconnect,
  (v) => v.utils.is_connected
);

function MakeBeacon() {
  const network = useNetwork()
  const endpoint = useEndpoint()
  const name = useAppName()
  const ttk = useTezosToolkit()

  const [beaconState, setState] = React.useState(() : {
    beacon       : undefined | BeaconWallet,
    user_address : undefined | string,
    wallet       : undefined | string,
  } => ({
    beacon       : new BeaconWallet({ name : name, preferredNetwork : network }),
    user_address : undefined,
    wallet       : undefined
  }));

  React.useEffect(() => {
    // to be executed on mount
    ttk.setWalletProvider(beaconState.beacon)
    const on_mount = async () => {
      const account = await beaconState.beacon?.client.getActiveAccount();
      const address = account?.address;
      const peers = await beaconState.beacon?.client?.getPeers();
      const wallet_name = peers !== undefined ? peers[0].name : undefined;
      setState(s => { return { ...s, user_address : address, wallet : wallet_name }})
    }
    on_mount()
  }, []);

  const connect = async () => {
    try {
      let beacon : BeaconWallet | undefined = undefined
      if (beaconState.beacon) {
        beacon = beaconState.beacon
        ttk.setWalletProvider(beacon);
        await beacon.requestPermissions({
          network : {
            type : network,
            rpcUrl : endpoint
          }
        })
        const address = await beacon.getPKH();
        const peers = await beacon.client.getPeers()
        const wallet_name = peers[0].name
        setState(s => { return { ...s, user_address : address, wallet : wallet_name }})
      } else {
        throw new Error("Beacon Not Initialized")
      }
    } catch(e) {
      console.log(e)
    }
  }

  const disconnect = async () => {
    beaconState.beacon?.client.disconnect()
    setState(s => { return { ...s,
      user_address : undefined
    }})
  }

  const is_connected = () => {
    return beaconState.user_address !== undefined
  }

  return { beaconState, utils : { connect, disconnect, is_connected } };
}

Usage

For example, the Login component providing the button to login to a wallet would code:

import { useConnect } from '../store/Beacon'

const LoginButton = () => {
  const connect = useConnect()
  return <Button onClick={connect} />
}

Contract.tsx

└── src
    └── store
        ├── Contract.tsx

This Contract context template provides the contract binding to components.

Code

import { set_binder_tezos_toolkit } from '@completium/dapp-ts';
import constate from 'constate';
import { useState } from 'react';

import { FIXME as Contract } from '../bindings/FIXME'; // replace FIXME
import { useContractAddress } from './Settings';
import { useTezosToolkit } from './Taquito';

export const [
  ContractProvider,
  useContract
] = constate(
  () => {
    const tezos = useTezosToolkit()
    const address = useContractAddress()
    const [contract] = useState({
      contract: new Contract(address),
    });
    set_binder_tezos_toolkit(tezos)
    return contract;
  },
  (v) => v.contract
)

Comments:

  1. replace FIXME with contract name; for example in the Poll dApp example:
  2. import { Poll as Contract } from '../bindings/poll';
  3. set_binder_tezos_toolkit provides dapp-ts package with the Tezos toolkit

Usage

import { useContract } from '../store/contract'

const contract = useContract()

See example dApp's detailed contract integration explanations.

App.tsx

└── src
    ├── App.tsx

The App.tsx file contains the root component of the application App (it comes with the default react template project).

The role of this component is to wrap the main panel component <MainPanel /> with constate context providers. It should at least be wrapped with the Settings, Taquito, Beacon and Contract contexts, as illustrated below:

function App() {
  return (
    <div className="App">
      <CssBaseline />
      <SettingsProvider>
        <TaquitoProvider>
          <BeaconProvider>
            <ContractProvider>
              <MainPanel />
            </ContractProvider>
          </BeaconProvider>
        </TaquitoProvider>
      </SettingsProvider>
    </div>
  );
}

Poll DApp

This section presents the Poll dApp example: users can create and answer polls. An account can only answer a poll once.

Poll definitions (question, image and possible answers) are stored on IPFS. The list of polls and user's answers are stored in a smart contract.

The dApp uses the MUI UI widget library.

Login

Click the topbar LOGIN button to connect to a wallet; select a wallet you want to connect to (Temple, Kukai, Umami, ...).

Once logged in, the login button is replaced by a panel with basic information:

  1. User address (tz1... or tz2...)
  2. Name of the wallet connected to
  3. Network (Ghostnet in this example)
  4. Balance
  5. Endpoint (the Tezos endpoint the dApp using)
  6. a LOGOUT button to disconnet from wallet

Answer a poll

Steps to answer a poll:

  • 1. click on a poll in the dApp's main panel
  • 2. click on one of the possible answers
  • 3. click the SUBMIT button; it is then required to connect to a wallet (if not already logged in) and validate the transaction to the contract

Once the transaction is validated on the blockchain, the statistic of the poll (number of answers per possible answer) is displayed.

Event notifications

User is notified when another user either creates a new poll or answers a poll:

  1. a message is displayed at the bottom of the screen for a few seconds
  2. the detailed event information is added to the notificationcenter

Create a poll

A poll is specified with a JSON file that defines the following fields:

  1. utterance: the question asked by the poll (for example "What is your favorite food?")
  2. img: a public URL to illustrate the question
  3. choices: a list of choices

or example, below is the "What is your favorite food?" poll definition:

{
  "utterance" : "What is your favorite food?",
  "img" : "https://1.bp.blogspot.com/-WOrNura8t1g/W6ufizUlixI/AAAAAAAAA38/X05agj6atWcxyyaRgOl07jMeaZnuF7JwgCLcBGAs/w1200-h630-p-k-no-nu/World-Food-Day.jpg",
  "choices" : [ "Pizza", "Burger", "Sushi", "Tacos", "Croissant" ]
}

The poll JSON file must be then uploaded to IPFS. An IPFS browser extension may be used for that. Once uploaded, the data is identified by a unique hash value.

For example, the hash of the above poll definition is QmZ8GxAwPvVDEtGxyUmfbB1dtmrdDR6tmMv9HUATaippqU

Steps to add a poll:

  • 1. click on the ADD POLL button in the dApp's main panel
  • 2. paste the hash value in the IPFS hash field; the poll is then displayed for validation
  • 3. click the SUBMIT button; it is then required to connect to a wallet (if not already logged in) and validate the transaction to the contract.

Once submitted, the contract's owner (tz1h4CiqWxNe4UxSpkwXy617RM6DaK6NU76P) needs to validate the poll for it to appear in the main panel.

Poll Contract

The poll contract stores polls' IPFS hash and users' answers. It guarantees that an account can only answer once to a poll. It also computes the number of times an answer has been selected.

When a user adds a poll (IPFS hash), it needs to be approved by a special account, called contract's owner. The owner can also remove any existing poll.

Project

Poll contract project was created with the following Completium CLI command:

completium-cli create project poll-contract

Command npm i installs required packages:

  1. typescript util packages
  2. mocha for test suite
  3. archetype's packages for binding

package.json file is created with utility commands, including:

  1. npm run "gen-binding" to generate contract(s)' binding
  2. npm test to launch tests in tests directory

Deployment

The following Completium CLI command is used to deploy the contract:

completium-cli deploy ./contracts/poll.arl --parameters '{ "owner": "tz1h4CiqWxNe4UxSpkwXy617RM6DaK6NU76P" }' --metadata-uri "ipfs://QmXbuUyyJXW1RRuL3k81Kpe2HULbYLj1sUUq44Nuxa5z8h"

where QmXbuUyyJXW1RRuL3k81Kpe2HULbYLj1sUUq44Nuxa5z8h is the IPFS hash of the contract metadata file:

{
  "name": "Poll Dapp",
  "description": "An example of Dapp built with Archetype & Completium",
  "version": "2.0",
  "license": { "name": "MIT" },
  "authors": ["Completium team <contact@completium.com>"],
  "homepage": "https://completium.github.io/poll-dapp/",
  "interfaces": ["TZIP-016"]
}

Storage

owner

Contract owner's address, passed as contract parameter. Only the owner can:

  1. approve a poll
  2. remove a poll
  3. transfer contract ownership
  4. pause contract
  5. unpause contract

Code

archetype poll(owner : address)
    with metadata ""

poll_counter

Number of polls added, used as poll key in poll asset collection (see approve entry point).

Code

variable polls_counter : nat = 0

poll

Collection of polls.

A poll is identified by a natural integer rather than by its IPFS hash. This is to minimize the required storage of the responder information, that stores which polls an user has answered.

The responses field stores the numbers of responses to poll's possible answers.

Code

asset poll {
          poll_pk   : nat;
          ipfs_hash : bytes;
          creation  : date          = now;
          responses : map<nat, nat> = []
        }

poll_to_approve

Collection of polls' IPFS hashes proposed by users. When approved by owner, a poll asset is created.

Note that the asset collection is created as a big_map, to be able to handle an arbitrary large amount of poll proposition.

Code

asset poll_to_approve to big_map {
          ipfs_hash_to_approve : bytes;
          poll_creator         : address = caller
        }

responder

Collection of responders' lists (set) of answered polls. This is to decide whether a responder has already answered a poll or not (see respond entrypoint).

It is specified as a big_mapto be able to handle an arbitrary large number of responders.

Code

asset responder to big_map {
          res_addr : address;
          polls    : set<nat> = [];
        }

Entry

add_poll

Entry to call to propose a new poll. The poll's IPFS hash is added to the collection of hashes to approve.

Code

entry add_poll(h : bytes) {
          require {
            r1 : is_not_paused()
          }
          effect {
            poll_to_approve.add({ ipfs_hash_to_approve = h });
            emit<NewPoll>({ caller; h })
          }
        }

Parameter

h : bytes

Poll's IPFS hash


CONTRACT_PAUSED

When contract has been paused by owner

Fails with

("KEY_EXISTS", "poll_to_approve")


When poll's IPFS hash h has already been proposed

Emits

respond

Entry to call to answer a poll. It fails if:

  1. the poll hash is not registered
  2. the caller has already responded

The number of times someone has responded to the poll's answer (choice_id) is incremented, and the poll id is registered in the set of polls caller has already responded to.

Code

entry respond(pk : nat, choice_id : nat) {
          constant {
            selection_count is poll[pk] ? (the.responses[choice_id] ? the : 0) : 0;
          }
          require {
            r2 : is_not_paused();
            r3 : poll.contains(pk) otherwise POLL_NOT_FOUND;
          }
          fail if {
            f1 : responder[caller] ? the.polls.contains(pk) : false with CANNOT_RESPOND_TWICE
          }
          effect {
            responder.add_update(caller, { polls += [pk] } );
            poll.update(pk, {
              responses += [(choice_id, selection_count + 1)]
            });
            emit<Response>({ caller; pk; choice_id })
          }
        }

Parameters

pk : nat

Poll's primary key

choice_id : nat

Poll's choice id selected by user


CONTRACT_PAUSED

When contract has been paused by owner

Fails with

POLL_NOT_FOUND

When poll's primary key pk is not found in poll asset

CANNOT_RESPOND_TWICE

When poll's primary key pk is found in caller's set of already responded polls

Called by owner

approve

Entry called by owner to approve a proposed poll:

  1. a new poll is added to the poll asset collection
  2. the proposed IPFS hash is removed from poll_to_approve

Code

entry approve(h : bytes) {
          called by owner
          constant {
            creator_ ?is poll_to_approve[h]?.poll_creator otherwise POLL_NOT_FOUND
          }
          effect {
            poll.add({ poll_pk = polls_counter; ipfs_hash = h });
            polls_counter += 1;
            poll_to_approve.remove(h);
            emit<Approve>({ creator_; h })
          }
        }

Parameter

h : bytes

Poll's IPFS hash

Fails with

POLL_NOT_FOUND

When poll's IPFS hash h is not found in poll_to_approve asset

Emits

disapprove

Entry called by owner to disapprove a proposed poll:

  1. the proposed IPFS hash is removed from poll_to_approve

Code

entry disapprove(h : bytes) {
          called by owner
          effect {
            poll_to_approve.remove(h)
          }
        }

Parameter

Poll's IPFS hash

Fails with
does not fail

remove

Entry called by owner to remove a poll.

Code

entry remove(pk : nat) {
          called by owner
          effect {
            poll.remove(pk)
          }
        }

View

get_responses

Returns poll pk response statistics.

Code

view get_responses(pk : nat) : map<nat, nat> {
          return poll[pk].responses
        }

already_responded

Returns true if sourcesource has already answered poll pk.

Code

view already_responded(pk : nat) : bool {
          return (responder[source] ? the.polls.contains(pk) : false)
        }

Events

NewPoll

Emitted by add_poll with:

  1. poll creator's address
  2. poll's IPFS hash

Code

event NewPoll {
          creator : address;
          poll_id : bytes
        }

Response

Emitted by respond with:

  1. responder's address
  2. poll's id
  3. response's id

Code

event Response {
          responder_addr : address;
          poll_id : nat;
          response : nat
        }

Approval

Emitted by approve with:

  1. proposal issuer's address
  2. poll's IPFS hash

Code

event Approval {
          creator : address;
          poll_id : bytes
        }

Contract Integration

This section presents in detail the interaction between the dApp's UI and the poll contract:

  1. how polls data are retrieved from contract?
  2. how entries and views are invoked?

Generate bindings

The following Completium CLI command generates the contract(s) bindings for a dApp:

completium-cli generate binding-dapp-ts ./poll-contract/contracts/poll.arl > ./src/bindings/poll.ts

Retrieving poll data

The Typescript binding provides the method get_poll that returns the poll container as a list of pair of poll key and poll_value, whose type finally reduces to:

Array<[ Nat, {
  ipfs_hash : Bytes,
  responses : Array<[ Nat, Nat ]>,
  creation  : Date
}]>;

The binding is using types from @completium/archtype-ts-types package:

  1. Nat to represent Michelson's natural integer type nat of arbitrary precision
  2. Bytes to represent bytes values

It is convienient to:

  1. downcast Nat values to native TypeScript values typed number
  2. turn Hex-encoded IPFS hash to poll definition's fields (utterance and choices)

A dedicated type Poll is created to merge contract's polls data with IPFS stored poll definition:

export interface Poll {
  id        : number,
  utterance : string,
  img       : string,
  choices   : Array<string>
  creation  : Date,
  responses : Array<[ number, number]>
}

The load_data function from Polls.tsx:

  • 1. loads data from contract
  • 2. maps each poll data to the Poll Typescript
const loadData = async () => {
  const poll_data = await contract.get_poll()
  const polls     = await Promise.all(poll_data.map(async ([poll_id, poll_value]) => {
    const url = ipfs + poll_value.ipfs_hash.hex_decode()
    const res = await fetch(url)
    const ui : UIPoll = await res.json()
    return {
      id        : poll_id.to_number(),
      utterance : ui.utterance,
      img       : ui.img,
      choices   : ui.choices,
      creation  : poll_value.creation,
      responses : poll_value.responses.map(x => [ x[0].to_number(), x[1].to_number() ])
    }
  }))
  setPolls(polls.sort((p1,p2) => p1.creation.getTime() - p2.creation.getTime()))
}

Comments:

  1. contrat object has been locally obtained with hook useContract from PollContract.tsx
  2. async method get_poll is used to retrieve poll container
  3. Nat's to_number method is used to downcast to native type number
  4. Bytes's hex_decode method is used to decode the IPFS hash
  5. polls are sorted by creation date to present more recent polls first

Invoking respond entry

RespondPoll UI component displays the SUBMIT button that calls contract's respond entry point.

The code that handles the click button event is:

const respond = async () => {
  try {
    if (choice !== undefined) {
      await contract.respond(new Nat(selected), new Nat(choice), {})
    }
  } catch(e) {
    console.log(e)
  }
}
<Button onClick={respond}>submit</Button>

Comments:

  1. contract object has been locally obtained with hook useContract from PollContract.tsx
  2. poll identifier selected and answer identifier choice are upcasted to Nat type as specified by contract's entry respond
  3. last argument of respond method is the call's optional parameters (typically the amount of tez to send the contract)

Invoking add_poll entry

AddPoll UI component displays the SUBMIT button that calls contract's add_poll entry point.

The code that handles the click button event is:

const add_poll = async () => {
  try {
    await contract.add_poll(Bytes.hex_encode(uri), {})
  } catch (e) {
    console.log(e)
  }
}
<Button onClick={add_poll}>submit</Button>
  1. contract object has been locally obtained with hook useContract from PollContract.tsx
  2. poll's definition IPFS hash is converted to bytes with Bytes utility method hex_encode, as specified by the contract's entry add_poll
  3. last argument of respond method is the call's optional parameters (typically the amount of tez to send the contract)

Invoking already_responded view

When loaded, RespondPoll UI component checks whether wallet address has already responded to the poll by invoking already_responded view. If so, poll statistics are displayed:

const RespondPoll = () => {
  useEffect(() => {
    const responded = await contract.view_already_responded(
      new Nat(poll.id),
      { as : new Address(wallet_address) }
    )
    if (responded) {
      await loadResponses(poll.id)
    }
  }, [])
}

Comments:

  1. useEffect React hook is invoked after UI component is loaded
  2. contract object has been locally obtained with hook useContract from PollContract.tsx
  3. call parameter provides the as field to set the source value used by the view

Listening to events

The poll contracts emits events on poll addition/approval and when a response is submitted. When an event is emitted, the dApp notifies the user with a snack message and a notification appears in the events' panel.

Contract's binding offers methods to register event's handlers. An event handler is a function called when an event is emitted, and that takes this new event as argument. It takes a second optional argument that provides blockchain-related information:

  1. emitter contract address
  2. block hash
  3. operation hash
  4. operation timestamp
  5. event name

Event handlers are registered in the useEffect hook of constate Events component:

useEffect(() => {
  const startListener = async () => {
    contract.register_Response(async (e : Response, d ?: EventData) => {
      setAlertMsg(make_response_msg(e))
      setAlerOpen(true)
      await loadResponses(e.poll_id.to_big_number().toNumber())
      if (d) addEvent(d)
    })
    contract.register_NewPoll((np : NewPoll, d ?: EventData) => {
      setAlertMsg(make_new_poll_msg(np))
      setAlerOpen(true)
      if (d) addEvent(d)
    })
    contract.register_ApprovePoll((ap : ApprovePoll, d ?: EventData) => {
      setAlertMsg(make_poll_confirmed_msg(ap))
      setAlerOpen(true)
      if (d) addEvent(d)
    })
    await run_listener({
      endpoint: endpoint,
      verbose: false,
      horizon: 0
    })
  };
  startListener()
}, [])

Comments:

  1. register_Response, register_NewPoll and register_Approve are binder's event handler registration methods
  2. run_listener is the function to start the event listener process (provided by @completium/event-listener package)

APIs

Store

Settings.tsx

useTheme
Theme mode (Light or Dark)
useEndpoint
Tezos endpoint URL
useContractAddress
Poll contract address (KT1...)
useNetwork
Network (for ex. Ghostnet) for Beacon to use
useIPFSBrowser
IPFS endpoint URL
useGitRepo
Code repository URL
useSetTheme
Theme setter

Taquito.tsx

useTezosToolkit
Taquito's Tezos Toolkit

Beacon.tsx

useWalletAddress
Wallet address (or undefined if not logged in)
useWalletName
Wallet name (or undefined if not logged in)
useIsConnected
Function that returns true if user is logged in, false otherwise
useConnect
Beacon's connect service
useDisconnect
Beacon's disconnect service

Contract.tsx

useContract
Returns contract binder

Polls.tsx

usePolls
List of polls
useLoadData
Function to fetch polls' data from contract
useLoadResponses
Function to fetch polls' responses from contract via the get_responses view

Alerts.tsx

useAlertOpen
Alert state (open/close) otherwise
useAlertMsg
Alert message to display
useAlertSetOpen
Function to set alert state
useAlertSetMsg
Function to set alert message

Events.tsx

useEvents
List of events
useNbNewEvents
Number of new events
useAddEvent
Function to add a new event to the notification menu
useClearEvents
Function to remove all events

UI hierarchy

.
└── App
    └── DApp
        ├── Router
        │   ├── AddPage
        │   │   ├── AddForm
        │   │   └── PollPreview
        │   │       └── PollPanel
        │   ├── PickPage
        │   │   └── [PollCard]
        │   └── PollPage
        │       └── PollPanel
        │           └── [ChoicePanel]
        └── TopBar
            ├── EventNotifications
            │   ├── [EventCard]
            │   └── NotificationMenu
            ├── GitHubLink
            ├── LoginButton
            │   └── WalletInfo
            ├── TezosIcon
            └── ThemeSwitch

Contract's binding

Interacting with the poll contract is done via the generated bindings.

The following Completium CLI command generates the contract bindings:

completium-cli generate ./poll_contract/contracts/poll.arl > ./src/binding/poll.ts

The following mockup code presents the generated Poll TypeScript API:

class Poll {
  /*
    Contract address is passed to the constructor
  */
  constructor Poll(address?: string | undefined): Poll
  // utils
  get_address(): Address
  get_balance(): Promise<Tez>
  /*
    Entries from Ownership template
  */
  declare_ownership(candidate: Address, params: Partial<Parameters>): Promise<any>
  claim_ownership(params: Partial<Parameters>): Promise<any>
  /*
    Entries from Pausable template
  */
  pause(params: Partial<Parameters>): Promise<any>
  unpause(params: Partial<Parameters>): Promise<any>
  /*
    Entries from Metadata template
  */
  set_metadata(k: string, d: Option<Bytes>, params: Partial<Parameters>): Promise<any>
  /*
    Poll specific entries
  */
  add_poll(h: Bytes, params: Partial<Parameters>): Promise<any>
  approve(h: Bytes, params: Partial<Parameters>): Promise<any>
  disapprove(h: Bytes, params: Partial<Parameters>): Promise<any>
  remove(pk: Nat, params: Partial<Parameters>): Promise<any>
  respond(pk: Nat, choice_id: Nat, params: Partial<Parameters>): Promise<any>
  /*
    Entries' CallParameter makers for batch operations
  */
  get_declare_ownership_param(candidate: Address, params: Partial<Parameters>): Promise<CallParameter>
  get_claim_ownership_param(params: Partial<Parameters>): Promise<CallParameter>
  get_pause_param(params: Partial<Parameters>): Promise<CallParameter>
  get_unpause_param(params: Partial<Parameters>): Promise<CallParameter>
  get_set_metadata_param(k: string, d: Option<Bytes>, params: Partial<Parameters>): Promise<CallParameter>
  get_add_poll_param(h: Bytes, params: Partial<Parameters>): Promise<CallParameter>
  get_approve_param(h: Bytes, params: Partial<Parameters>): Promise<CallParameter>
  get_disapprove_param(h: Bytes, params: Partial<Parameters>): Promise<CallParameter>
  get_remove_param(pk: Nat, params: Partial<Parameters>): Promise<CallParameter>
  get_respond_param(pk: Nat, choice_id: Nat, params: Partial<Parameters>): Promise<CallParameter>
  /*
    Views
  */
  view_get_responses(pk: Nat, params: Partial<Parameters>): Promise<Array<[ Nat, Nat ]>>
  view_already_responded(pk: Nat, params: Partial<Parameters>): Promise<boolean>
  /*
    Storage elements getters
  */
  get_owner(): Promise<Address>
  get_owner_candidate(): Promise<Option<Address>>
  get_paused(): Promise<boolean>
  get_polls_counter(): Promise<Nat>
  get_poll(): Promise<poll_container>
  get_poll_to_approve_value(key: poll_to_approve_key): Promise<poll_to_approve_value | undefined>
  has_poll_to_approve_value(key: poll_to_approve_key): Promise<boolean>
  get_responder_value(key: responder_key): Promise<responder_value | undefined>
  has_responder_value(key: responder_key): Promise<boolean>
  get_metadata_value(key: string): Promise<Bytes | undefined>
  has_metadata_value(key: string): Promise<boolean>
  register_Response(ep: el.EventProcessor<Response>): void
  /*
    Event register utils
  */
  register_NewPoll(ep: el.EventProcessor<NewPoll>): void
  register_ApprovePoll(ep: el.EventProcessor<ApprovePoll>): void
  /*
    Errors
  */
  (property) Poll.errors: {
    f1: Micheline;
    r3: Micheline;
    r2: Micheline;
    INVALID_CALLER: Micheline;
    POLL_NOT_FOUND: Micheline;
    r1: Micheline;
    md_r1: Micheline;
    pausable_r2: Micheline;
    pausable_r1: Micheline;
    ownership_r1: Micheline;
    CONTRACT_PAUSED: Micheline;
  }
}

Stay up to date on all things Tezos