Skip to main content
Version: Next

Part 1: Creating a contract

Meet Pedro, our artisan taco chef, who has decided to open a Taco shop on the Tezos blockchain, using a smart contract.

In this tutorial, to help Pedro open his dream taco shop, you will implement a smart contract that manages supply, pricing, and sales of his tacos to the consumers. This scenario is ideal for a smart contract because smart contracts behave much like vending machines: users send requests to them along with information and money. If the request is correct, the smart contract does something in response, in this case giving the customer an imaginary taco.


Made by Smashicons from www.flaticon.com is licensed by CC 3.0 BY

Learning objectives

In this tutorial, you will learn how to:

  • Set up a smart contract in JsLIGO or CameLIGO
  • Define the storage for the contract
  • Define what requests the contract can accept and how it behaves
  • Implement the code that handles these requests
  • Write tests that ensure that the contract behaves correctly

Prerequisites

Before you begin, install LIGO as described in Installation.

Optionally, you can also set up your editor to work with LIGO as described in Editor Support.

Syntaxes

LIGO has two syntaxes:

  • JsLIGO is inspired by TypeScript/JavaScript, intended for web developers

  • CameLIGO is inspired by OCaml, intended for functional programmers

The syntaxes do the same thing and have nearly all the same features, so which one you choose depends on your preference or programming background. You can use either syntax for this tutorial, but you must use the same syntax for the entire contract. Use the Syntax Preference slider at the top left of this page to select the syntax to use.

Pricing

Pedro sells two kinds of tacos: el Clásico and the Especial del Chef. His tacos are a rare delicacy and he has a finite amount of each kind, so the price goes up as the stock for the day depletes. Taco prices are in tez, the currency of the Tezos blockchain.

The cost for one taco is the maximum price for the taco divided by the total number of tacos, as in this formula:

current_purchase_price = max_price / available_stock

For example, the maximum price for an el Clásico taco is 50 tez. This table shows the price when there are certain amounts of tacos left:

Number of tacos availableMaximum pricePurchase price
5050 tez1 tez
2050 tez2.5 tez
550 tez10 tez
150 tez50 tez

The maximum price for an Especial del Chef taco is 75 tez, so the prices are different, as in this table:

Number of tacos availableMaximum pricePurchase price
2075 tez3.75 tez
1075 tez7.5 tez
575 tez15 tez
175 tez75 tez

Setting up the data storage

Smart contracts can store persistent data. Only the contract itself can write to its data, but the data is visible to outside users. This data can be in many data types, including simple data types like numbers, Boolean values, and strings, and complex data types like arrays and maps.

Because the cost of a taco is determined by a formula, the contract needs to store only two pieces of data for each type of taco: the maximum price and the number of tacos currently in stock. LIGO contracts store this type of data in a data type called a map, which is a key-value store where each key is the same data type and each value is the same data type. Maps are flexible, so you can add and remove elements.

The key for this map is a natural number (also known as a nat, an integer zero or greater) and the value is a Record data type that has two fields: a natural number for the current stock of tacos and a tez amount for the maximum price. In table format, the map data looks ike this:

KeyValue
1{ current_stock: 50, maximum_price: 50tez }
2{ current_stock: 20, maximum_price: 75tez }

Follow these steps to set up the data storage for your contract:

  1. Anywhere on your computer, create a folder to store your work for this tutorial with a name such as TacoShopTutorial.

  2. In the folder, create a file named taco_shop.jsligo to store the code of the smart contract. You can create and edit this file in any text editor.

  3. In the file, create a type named taco_supply that represents the value of the map, consisting of a nat for the number of tacos and a tez value for the maximum price:

    export type taco_supply = { current_stock: nat, max_price: tez };
  4. Create a map type named taco_data, with the key a nat and the value the taco_supply type:

    export type taco_data = map<nat, taco_supply>;

    This map can contain the supply and max price for any number of tacos, indexed by a natural number key.

  5. Create an address type to store Pedro's account address, which allows him to lock some features of the contract behind an administrator account:

    export type admin_address = address;
  6. Create a type to represent the storage for the contract. In this case, the contract needs to store the taco data map and the administrator address, so the overall contract storage contains those two values:

    export type storage = {
    admin_address: admin_address,
    taco_data: taco_data,
    };
  7. Create a constant to represent the starting values for the taco data map:

    export const default_taco_data: taco_data = Map.literal([
    [1 as nat, { current_stock: 50 as nat, max_price: 50 as tez }],
    [2 as nat, { current_stock: 20 as nat, max_price: 75 as tez }]
    ]);

    Note that the natural numbers are indicated with an as nat after the number; otherwise, LIGO assumes that numbers are integers. Similarly, the maximum prices of the tacos have as tez to indicate that they are amounts of tez.

  8. To keep the code for the contract organized, put the types and values in a namespace named TacoShop. The contract looks like this so far:

    namespace TacoShop {
    export type taco_supply = { current_stock: nat, max_price: tez };
    export type taco_data = map<nat, taco_supply>;
    export type admin_address = address;
    export type storage = {
    admin_address: admin_address,
    taco_data: taco_data,
    };
    export const default_taco_data: taco_data = Map.literal([
    [1 as nat, { current_stock: 50 as nat, max_price: 50 as tez }],
    [2 as nat, { current_stock: 20 as nat, max_price: 75 as tez }]
    ]);
    };

Getting the price of tacos

Because the price of tacos changes, it'll be helpful to have a function to get the current price of a certain kind of taco.

Add this function inside the namespace, immediately after the default_taco_data constant:

// Internal function to get the price of a taco
const get_taco_price_internal = (taco_kind_index: nat, taco_data: taco_data): tez => {
const taco_kind: taco_supply =
$match (Map.find_opt(taco_kind_index, taco_data), {
"Some": (kind) => kind,
"None": () => failwith("Unknown kind of taco"),
});
return taco_kind.max_price / taco_kind.current_stock;
}

This code uses the Map.find_opt function to get an entry from a map based on a key. It returns an option value, which is a data type that LIGO uses to handle cases where a value may not exist. In this case, the option has the value for that key if the key exists or a None value if the key does not exist. If the taco_kind_index parameter is not a valid taco ID, the transaction fails.

This is an internal function, so external callers can't call it directly. Later, you will add a way for external callers to get the current price of a taco.

Selling tacos

Contracts have one or more entrypoints, which are a kind of function that clients can call, like endpoints in an API or functions or methods in many other programming languages. A contract can have any number of internal functions, but only the functions designated as entrypoints can be called by outside consumers and other contracts.

The contract you create in this tutorial has two entrypoints:

  • An entrypoint named buy_taco which accepts the type of taco to buy and the price of the taco and deducts that type of taco from the current stock in storage
  • An entrypoint named payout that sends the tez in the contract to Pedro and restocks the supply of tacos

As described in Entrypoints, entrypoints must follow a specific signature to be compiled as entrypoints:

  • Entrypoints are functions marked with the @entry decorator, which (when used in a namespace) must be in a comment immediately before the function
  • Entrypoints receive a parameter from the caller and the current state of the contract storage
  • Entrypoints return a tuple consisting of a list of operations to run (such as calls to other smart contracts or transfers of tez) and the new state of the contract storage
  1. In the smart contract file, within the TacoShop namespace, add this stub of an entrypoint:

    // Buy a taco
    // @entry
    const buy_taco = (taco_kind_index: nat, storage: storage): [
    list<operation>,
    storage
    ] => {
    // Entrypoint logic goes here
    return [[], updated_storage];
    }

    Your IDE may show an error that the updated_storage value is not defined, but you can ignore this error for now because you will define it in the next few steps.

    To call this entrypoint, the caller passes a nat to indicate the type of taco. The function automatically receives the current state of the storage as the last parameter. The line return [[], updated_storage]; returns an empty list of operations to run and the new state of the storage. In the next few steps, you add logic to verify that the caller sent the correct price and to deduct the taco from the current stock.

  2. Within the entrypoint, add code to get the admin address and the taco data by destructuring the storage parameter:

    const { admin_address, taco_data } = storage;
  3. After this code, add code to get the type of taco that the caller requested based on the taco_kind_index parameter:

    // Retrieve the kind of taco from the contracts storage or fail
    const taco_kind: taco_supply =
    $match (Map.find_opt(taco_kind_index, taco_data), {
    "Some": (kind) => kind,
    "None": () => failwith("Unknown kind of taco"),
    });
  4. After the code you just added, add this code to get the current price of a taco:

    // Get the current price of this type of taco
    const current_purchase_price = get_taco_price_internal(taco_kind_index, taco_data);
  5. Add this code to verify that the caller sent the correct amount of tez with the transaction. It uses the Tezos.get_amount() function, which returns the amount of tez that the caller sent:

    // Verify that the caller sent the correct amount of tez
    if ((Tezos.get_amount()) != current_purchase_price) {
    return failwith("Sorry, the taco you are trying to purchase has a different price");
    }
  6. Add this code to verify that there is at least one taco in stock:

    // Verify that there is at least one of this type of taco
    if (taco_kind.current_stock == 0 as nat) {
    return failwith("Sorry, we are out of this type of taco");
    }
  7. Add this code to calculate the updated taco data map and put it in the updated_taco_data constant:

    // Update the storage with the new quantity of tacos
    const updated_taco_data: taco_data = Map.update(
    taco_kind_index,
    ["Some" as "Some", {...taco_kind, current_stock: abs(taco_kind.current_stock - 1) }],
    taco_data);

    This code uses the Map.update function to create a new version of the map with an updated record. In this case, the new map updates the stock of the specified type of taco to be one less. It uses the abs function to ensure that the new stock of tacos is a nat, because subtraction yields an integer.

  8. Create the new value of the contract storage, including the admin address and the updated taco data:

    const updated_storage: storage = {
    admin_address: admin_address,
    taco_data: updated_taco_data,
    };

    The next line is the line return [[], updated_taco_data];, which you added when you stubbed in the entrypoint code earlier.

    Now the buy_taco entrypoint updates the stock in storage to indicate that it has one less of that type of taco. The contract automatically accepts the tez that is included with the transaction.

  9. After the code for the buy_taco entrypoint, stub in the code for the entrypoint that allows Pedro to retrieve the tez in the contract, which you will add in a later section:

    // @entry
    const payout = (_u: unit, storage: storage): [
    list<operation>,
    storage
    ] => {
    // Entrypoint logic goes here
    return [[], storage];
    }

    Currently this entrypoint does nothing, but you will add code for it later.

Providing information to clients

Earlier, you added an internal function that calculated the price of a taco. External clients can't call this function because it is private to the contract.

The contract should give Pedro's customers a way to get the current price of a taco. However, because entrypoints don't return a value directly to the caller, an entrypoint isn't the best way to provide information to clients.

If you need to provide information to clients, one way is to use a view, which is a static function that returns a value to clients but does not change the storage or generate any operations. Like entrypoints, views are functions that receive one or more parameters from the caller and the current value of the storage. Unlike entrypoints, they return a single value to the caller instead of a list of operations and the new value of the storage.

Add this view to the contract, after the get_taco_price_internal function and somewhere within the namespace:

// @view
const get_taco_price = (taco_kind_index: nat, storage: storage): tez =>
get_taco_price_internal(taco_kind_index, storage.taco_data);

This view is merely a wrapper around the get_taco_price_internal function, but the @view decorator makes external clients able to call it.

For more information about views, see Views.

The complete contract file looks like this:

namespace TacoShop {
export type taco_supply = { current_stock: nat, max_price: tez };
export type taco_data = map<nat, taco_supply>;
export type admin_address = address;
export type storage = {
admin_address: admin_address,
taco_data: taco_data,
};
export const default_taco_data: taco_data = Map.literal([
[1 as nat, { current_stock: 50 as nat, max_price: 50 as tez }],
[2 as nat, { current_stock: 20 as nat, max_price: 75 as tez }]
]);
// Internal function to get the price of a taco
const get_taco_price_internal = (taco_kind_index: nat, taco_data: taco_data): tez => {
const taco_kind: taco_supply =
$match (Map.find_opt(taco_kind_index, taco_data), {
"Some": (kind) => kind,
"None": () => failwith("Unknown kind of taco"),
});
return taco_kind.max_price / taco_kind.current_stock;
}
// @view
const get_taco_price = (taco_kind_index: nat, storage: storage): tez =>
get_taco_price_internal(taco_kind_index, storage.taco_data);
// Buy a taco
// @entry
const buy_taco = (taco_kind_index: nat, storage: storage): [
list<operation>,
storage
] => {
const { admin_address, taco_data } = storage;
// Retrieve the kind of taco from the contracts storage or fail
const taco_kind: taco_supply =
$match (Map.find_opt(taco_kind_index, taco_data), {
"Some": (kind) => kind,
"None": () => failwith("Unknown kind of taco"),
});
// Get the current price of this type of taco
const current_purchase_price = get_taco_price_internal(taco_kind_index, taco_data);
// Verify that the caller sent the correct amount of tez
if ((Tezos.get_amount()) != current_purchase_price) {
return failwith("Sorry, the taco you are trying to purchase has a different price");
}
// Verify that there is at least one of this type of taco
if (taco_kind.current_stock == (0 as nat)) {
return failwith("Sorry, we are out of this type of taco");
}
// Update the storage with the new quantity of tacos
const updated_taco_data: taco_data = Map.update(
taco_kind_index,
["Some" as "Some", {...taco_kind, current_stock: abs(taco_kind.current_stock - 1) }],
taco_data);
const updated_storage: storage = {
admin_address: admin_address,
taco_data: updated_taco_data,
};
return [[], updated_storage];
}
// @entry
const payout = (_u: unit, storage: storage): [
list<operation>,
storage
] => {
// Entrypoint logic goes here
return [[], storage];
}
};

Compiling the contract

Before you can deploy the contract to Tezos, you must compile it to Michelson,the low-level language of contracts on Tezos.

Run this command to compile the contract:

ligo compile contract -m TacoShop -o taco_shop.tz taco_shop.jsligo

If compilation is successful, LIGO prints nothing to the console and writes the compiled contract to the file taco_shop.tz. You don't need to interact with this file directly.

If you see errors, make sure your code matches the code in the previous section.

You now have a basic contract that can accept requests to sell tacos. However, before you deploy it, you should test the contract to make sure it works. Continue to Part 2: Testing the contract.