Advance

 

Building a Dapp on Taquito

Welcome to our tutorial on Taquito, a powerful JavaScript library for interacting with the Tezos blockchain network. In this tutorial, we will cover the basics of using Taquito to interact with the Tezos blockchain, including creating and managing accounts, transferring tokens, and interacting with smart contracts. We will also explore the various features and functionality of Taquito, such as signing and broadcasting transactions, and querying the blockchain.

By the end of this tutorial, you will have the knowledge and hands-on experience needed to use Taquito to interact with the Tezos blockchain. You will also learn how to use Taquito to perform common tasks such as creating and managing accounts, transferring tokens, and interacting with smart contracts. This tutorial is ideal for developers and researchers interested in interacting with the Tezos blockchain using JavaScript.

Interacting with the Tezos blockchain can be done using to the Tezos CLI. However, it is not suitable for Dapps since it needs to be integrated into web interfaces.

Fortunately, the Tezos ecosystem offers libraries in several languages that enable developers to build efficient Dapps. Taquito is one of these: it is a Typescript library developed and maintained by ECAD Labs. This library offers developers all of the everyday interactions with the blockchain: retrieving information about a Tezos network, sending a transaction, contract origination and interactions such as calling an entrypoint and fetching the storage, delegation, fetching metadata, etc.

Installation

The Taquito library is made of several modules:

The main module is @taquito/taquito, it will be used for most actions. The other modules are used by the @taquito/taquito methods as complementary features.

Let’s initialize a Typescript project and install taquito:

$ mkdir taquito-poc
$ mkdir taquito-poc/src
$ touch taquito-poc/src/app.ts taquito-poc/main.ts
$ cd taquito-poc
$ npx typescript --init --resolveJsonModule
$ yarn add typescript @taquito/taquito

The main.ts file will import an App class from src/app.ts and run its main function:

// main.ts
import { App } from './src/app';

new App().main();

Let’s create the App class with a main method. We import the TezosToolkit class to check if @taquito/taquito is indeed installed:

// src/app.ts
import { TezosToolkit } from '@taquito/taquito';

export class App {
    public async main() { }
}

Let’s run it with:

$ npx ts-node main.ts

If Taquito is correctly installed, this should not raise any exceptions.

Taquito configuration

We first need to configure Taquito with an RPC URL (to communicate with a Tezos node).

To do that we use the TezosToolkit: it is the “facade class that surfaces all of the libraries capability and allows its configuration”. When created, it accepts an RPC URL.

Here, we will use the Hangzhou testnet RPC URL offered for free by at https://hangzhounet.smartpy.io.

// src/app.ts
import { TezosToolkit } from '@taquito/taquito';

export class App {
    private tezos: TezosToolkit;

    constructor(rpcUrl: string) {
        this.tezos = new TezosToolkit(rpcUrl);
    }

    public async main() { }
}
// main.ts
import { App } from './src/app';

const RPC_URL = "https://hangzhounet.smartpy.io";

new App(RPC_URL).main();

Interactions without an account

Taquito is already ready for some actions: it can retrieve all the information about the Tezos network, the accounts, and the smart contracts.

For instance, let’s retrieve the balance of an account, with the getBalance method:

// src/app.ts
import { TezosToolkit } from '@taquito/taquito';

export class App {
    private tezos: TezosToolkit;

    constructor(rpcUrl: string) {
        this.tezos = new TezosToolkit(rpcUrl);
    }

    public getBalance(address: string) : void {
        this.tezos.rpc
            .getBalance(address)
            .then(balance => console.log(balance))
            .catch(e => console.log('Address not found'));
    }

    public async main() { }
}

Every interaction with the Tezos network through Taquito is handled via a Javascript Promise.

Let’s call this method for the address: tz1Xqa5LRU5tayDcZEFr7Sw2GjrbDBY3HtHH

// main.ts
import { App } from './src/app';

const RPC_URL = "https://hangzhounet.smartpy.io";
const ACCOUNT_TO_CHECK = "tz1Xqa5LRU5tayDcZEFr7Sw2GjrbDBY3HtHH";

new App(RPC_URL).getBalance(ACCOUNT_TO_CHECK);

Let’s run it:

$ npx ts-node main.ts 
BigNumber { s: 1, e: 7, c: [ 79229894 ] }

Contract data

We can also retrieve the metadata and storage of a contract.

// src/app.ts
import { TezosToolkit } from '@taquito/taquito';

export class App {
    private tezos: TezosToolkit;

    constructor(rpcUrl: string) {
        this.tezos = new TezosToolkit(rpcUrl);
    }

    public getBalance(address: string) : void {
        this.tezos.rpc
            .getBalance(address)
            .then(balance => console.log(balance))
            .catch(e => console.log('Address not found'));
    }

    public getContractEntrypoints(address: string) {
        this.tezos.contract
            .at(address)
            .then((c) => {
                let methods = c.parameterSchema.ExtractSignatures();
                console.log(JSON.stringify(methods, null, 2));
            })
            .catch((error) => console.log(`Error: ${error}`));
    }

    public async main() { }
}

Let’s run it for the simple Counter contract on Hangzhounet.

// main.ts
import { App } from './src/app';

const RPC_URL = "https://hangzhounet.smartpy.io";
const ACCOUNT_TO_CHECK = "tz1Xqa5LRU5tayDcZEFr7Sw2GjrbDBY3HtHH";
const COUNTER_CONTRACT = "KT1Gm3XHrRCTJ3TNH6kB3Nt4pURv4XR3kTv8";

new App(RPC_URL).getContractEntrypoints(COUNTER_CONTRACT);

The output is:

$ npx ts-node main.ts 
[
  [
    "decrement",
    "int"
  ],
  [
    "increment",
    "int"
  ]
]

Interactions with an account

Taquito can also sign and send transactions, but it needs a private key to do that.

Sending a transaction

Let’s send some Tez to another address.

Transactions can be sent with this.tezos.contract.transfer. It returns a Promise<TransactionOperation>.

A TransactionOperation contains the information about this transaction. It also has a confirmation method. This method can wait for several confirmations on demand.

Let’s create a sendTz method that sends an amount of Tez to the recipient address.

// src/app.ts
import { TezosToolkit } from '@taquito/taquito';
import { InMemorySigner } from '@taquito/signer';

export class App {
    private tezos: TezosToolkit;

    constructor(rpcUrl: string) {
        this.rpcUrl = rpcUrl
        this.tezos = new TezosToolkit(rpcUrl);
        this.tezos.setSignerProvider(InMemorySigner('YOUR_PRIVATE_KEY'))
    }

    public sendTz(address: string, amount: number) {
        console.log(`Transfering ${amount} ꜩ to ${address}...`);

        this.tezos.contract.transfer({ to: address, amount: amount })
            .then(op => {
                console.log(`Waiting for ${op.hash} to be confirmed...`);
                return op.confirmation(1).then(() => op.hash);
            })
            .then(hash => console.log(`${hash}`))
            .catch(error => console.log(`Error: ${error} ${JSON.stringify(error, null, 2)}`));
    }
}

Let’s call it from our main.ts file:

// main.ts
import { App } from './src/app';

const RPC_URL = "https://hangzhounet.smartpy.io";
const ACCOUNT_TO_CHECK = "tz1Xqa5LRU5tayDcZEFr7Sw2GjrbDBY3HtHH";
const COUNTER_CONTRACT = "KT1BEMULAGQ58C5NNdWQM3WYLjUtwgJ8X8aN";
const RECIPIENT = "tz1dDc5HrFbjsAuydBwotTa2nzuRkePSRDZg";
const AMOUNT = 10;

new App(RPC_URL).sendTz(RECIPIENT, AMOUNT);

Let’s run it from the console:

$ npx ts-node main.ts 
Transfering 10 ꜩ to tz1dDc5HrFbjsAuydBwotTa2nzuRkePSRDZg...
Waiting for oohhG6GmrH2j1xARjnZ2Q3WFGYR3zPRzDzsAaFYb1TwdmJjyqV2 to be confirmed...
oohhG6GmrH2j1xARjnZ2Q3WFGYR3zPRzDzsAaFYb1TwdmJjyqV2

We can now check the transaction on an explorer (TzStats or TzKT).

Making a contract call

Taquito can call smart contracts as well. We will use the Counter contract. If you need to know what are the available entrypoints, you can use the getContractEntrypoints defined in the Contract data subsection.

Let’s call the increment entrypoint. It takes a single int as input.

To do so, we need:

1. To get the contract with this.tezos.contract.at(contract). It returns a Promise<ContractAbstraction<ContractProvider>>.

2. Get the entrypoints. For this ContractAbstraction<ContractProvider> has a methods property contraining the entrypoints increment and decrement.

3. Get the increment entrypoint with methods.increment(2) to increment the counter by 2.

4. Send the contract call and inspect the transaction with contract.methods.increment(i).send().

5. Wait for a chosen number of confirmations, let’s say 3.

// src/app.ts
import { TezosToolkit } from '@taquito/taquito';
import { InMemorySigner } from '@taquito/signer';

export class App {
    private tezos: TezosToolkit;

    constructor(rpcUrl: string) {
        this.tezos = new TezosToolkit(rpcUrl);
        this.tezos.setSignerProvider(InMemorySigner('YOUR_PRIVATE_KEY'));
    }

    public increment(increment: number, contract: string) {
        this.tezos.contract
            .at(contract) // step 1
            .then((contract) => {
                console.log(`Incrementing storage value by ${increment}...`);
                return contract.methods.increment(increment).send(); // steps 2, 3 and 4
            })
            .then((op) => {
                console.log(`Awaiting for ${op.hash} to be confirmed...`);
                return op.confirmation(3).then(() => op.hash); // step 5
            })
            .then((hash) => console.log(`Operation injected: https://hangzhounet.smartpy.io/${hash}`))
            .catch((error) => console.log(`Error: ${JSON.stringify(error, null, 2)}`));
    }
}

Let’s call it from our main.ts file:

// main.ts
import { App } from './src/app';

const RPC_URL = "https://hangzhounet.smartpy.io";
const ACCOUNT_TO_CHECK = "tz1Xqa5LRU5tayDcZEFr7Sw2GjrbDBY3HtHH";
const COUNTER_CONTRACT = "KT1Gm3XHrRCTJ3TNH6kB3Nt4pURv4XR3kTv8";
const RECIPIENT = "tz1dDc5HrFbjsAuydBwotTa2nzuRkePSRDZg";
const AMOUNT = 10;
const INCREMENT = 5;

new App(RPC_URL).increment(INCREMENT, COUNTER_CONTRACT);

The send() function can take an object with fields as an input, such as amount (which defines an amount sent with the contract call), storageLimit, etc.

Sending several transactions

Let’s consider this Dapp:

// src/app.ts
import { TezosToolkit } from '@taquito/taquito';
import { InMemorySigner } from '@taquito/signer';

export class App {
    private tezos: TezosToolkit;

    constructor(rpcUrl: string) {
        this.rpcUrl = rpcUrl
        this.tezos = new TezosToolkit(rpcUrl);
        this.tezos.setSignerProvider(InMemorySigner('YOUR_PRIVATE_KEY'))
    }

    public increment(increment: number, contract: string) {
        this.tezos.contract
            .at(contract) // step 1
            .then((contract) => {
                console.log(`Incrementing storage value by ${increment}...`);
                return contract.methods.increment(increment).send(); // steps 2, 3 and 4
            })
            .then((op) => {
                console.log(`Awaiting for ${op.hash} to be confirmed...`);
                return op.confirmation(3).then(() => op.hash); // step 5
            })
            .then((hash) => console.log(`Operation injected: https://florence.tzstats.com/${hash}`))
            .catch((error) => console.log(`Error: ${JSON.stringify(error, null, 2)}`));
    }

    public sendTz(address: string, amount: number) {
        console.log(`Transfering ${amount} ꜩ to ${address}...`);
        this.tezos.contract.transfer({ to: address, amount: amount })
            .then(op => {
                console.log(`Waiting for ${op.hash} to be confirmed...`);
                return op.confirmation(1).then(() => op.hash);
            })
            .then(hash => console.log(`${hash}`))
            .catch(error => console.log(`Error: ${error} ${JSON.stringify(error, null, 2)}`));
    }
}

This is basically a concatenation of the Counter example and the Transfer example. Now, let’s consider a use case where we need to send these two transactions at the same time (and maybe additional contract calls, originations, or transfer transactions). One could be tempted to make those calls one after the other like this:

// main.ts
import { App } from './src/app';

const RPC_URL = "https://hangzhounet.smartpy.io";
const ACCOUNT_TO_CHECK = "tz1Xqa5LRU5tayDcZEFr7Sw2GjrbDBY3HtHH";
const COUNTER_CONTRACT = "KT1Gm3XHrRCTJ3TNH6kB3Nt4pURv4XR3kTv8";
const RECIPIENT = "tz1dDc5HrFbjsAuydBwotTa2nzuRkePSRDZg";
const AMOUNT = 10;
const INCREMENT = 5;

const app : App = new App(RPC_URL);
app.increment(INCREMENT, COUNTER_CONTRACT);
app.sendTz(RECIPIENT, AMOUNT);

We basically make a contract call and then try to send some funds to an address. Here is the output:

$ npx ts-node main.ts 
Transfering 10 ꜩ to tz1dDc5HrFbjsAuydBwotTa2nzuRkePSRDZg...
Incrementing storage value by 5...
Waiting for ooEQVNe3SVJkG6TW8WvLbpFDrdtsau6ys7eb2g4nUTbVBUjdQYi to be confirmed...
Error: {
  "status": 500,
  "statusText": "Internal Server Error",
  "body": "[{\"kind\":\"temporary\",\"id\":\"failure\",\"msg\":\"Error while applying operation ooSRhMW4TgVbBa7XvMS18wa3X5CPsm4XSXZ1nKtb9KxdTL4HcQS:\\nError:\\n  Counter 3615454 already used for contract tz1Xqa5LRU5tayDcZEFr7Sw2GjrbDBY3HtHH (expected 3615455)\\n\"}]\n",
  "url": "https://hangzhounet.smartpy.io/injection/operation",
  "name": "HttpResponse"
}
ooEQVNe3SVJkG6TW8WvLbpFDrdtsau6ys7eb2g4nUTbVBUjdQYi

The meaningful part is Counter 3615454 already used for contract tz1Xqa5LRU5tayDcZEFr7Sw2GjrbDBY3HtHH. Each transaction in our Dapp is performed asynchronously: the application makes the contract call to the increment entrypoint, but did not wait for the confirmation to made the transfer transaction. The contract call transaction was still in the mempool when the transfer transaction was sent. Thus, it failed.

However, Taquito offers a batch method, which enables Dapps to send several transactions at once.

To do so, we need to:

1. Retrieve the contract that we want to call,

2. Call the batch method,

3. Use withTransfer and/or withContractCall,

4. Send the transactions batch,

5. Wait for their confirmation.

Here is an example:

public async sendInBatch(contractAddress: string, recipientAddress : string) {
    const contract = await this.tezos.contract.at(contractAddress) //step 1

    const batch = this.tezos.contract.batch() // step 2
        .withTransfer({ to: recipientAddress, amount: 10 }) // step 3
        .withTransfer({ to: recipientAddress, amount: 100 }) // step 3
        .withTransfer({ to: recipientAddress, amount: 1000 }) // step 3
        .withContractCall(contract.methods.increment(10)) // step 3

    const batchOp = await batch.send(); // step 4

    await batchOp.confirmation(); // step 5
}

Here is its output on TzKT and TzStats.

Our three transfer transactions and our contract call are now indeed batched together in an operation.

With this, you complete this workshop successfully!!