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.
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:
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 available | Maximum price | Purchase price |
|---|---|---|
| 50 | 50 tez | 1 tez |
| 20 | 50 tez | 2.5 tez |
| 5 | 50 tez | 10 tez |
| 1 | 50 tez | 50 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 available | Maximum price | Purchase price |
|---|---|---|
| 20 | 75 tez | 3.75 tez |
| 10 | 75 tez | 7.5 tez |
| 5 | 75 tez | 15 tez |
| 1 | 75 tez | 75 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:
| Key | Value |
|---|---|
| 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:
Anywhere on your computer, create a folder to store your work for this tutorial with a name such as
TacoShopTutorial.In the folder, create a file named
taco_shop.jsligoto store the code of the smart contract. You can create and edit this file in any text editor.In the file, create a type named
taco_supplythat 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 };Create a map type named
taco_data, with the key a nat and the value thetaco_supplytype: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.
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;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,};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 natafter the number; otherwise, LIGO assumes that numbers are integers. Similarly, the maximum prices of the tacos haveas tezto indicate that they are amounts of tez.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:
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_tacowhich 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
payoutthat 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
@entrydecorator, 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
In the smart contract file, within the
TacoShopnamespace, add this stub of an entrypoint:// Buy a taco// @entryconst buy_taco = (taco_kind_index: nat, storage: storage): [list<operation>,storage] => {// Entrypoint logic goes herereturn [[], updated_storage];}Your IDE may show an error that the
updated_storagevalue 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.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;After this code, add code to get the type of taco that the caller requested based on the
taco_kind_indexparameter:// Retrieve the kind of taco from the contracts storage or failconst taco_kind: taco_supply =$match (Map.find_opt(taco_kind_index, taco_data), {"Some": (kind) => kind,"None": () => failwith("Unknown kind of taco"),});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 tacoconst current_purchase_price = get_taco_price_internal(taco_kind_index, taco_data);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 tezif ((Tezos.get_amount()) != current_purchase_price) {return failwith("Sorry, the taco you are trying to purchase has a different price");}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 tacoif (taco_kind.current_stock == 0 as nat) {return failwith("Sorry, we are out of this type of taco");}Add this code to calculate the updated taco data map and put it in the
updated_taco_dataconstant:// Update the storage with the new quantity of tacosconst 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.updatefunction 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 theabsfunction to ensure that the new stock of tacos is a nat, because subtraction yields an integer.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_tacoentrypoint 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.After the code for the
buy_tacoentrypoint, 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:// @entryconst payout = (_u: unit, storage: storage): [list<operation>,storage] => {// Entrypoint logic goes herereturn [[], 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:
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:
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:
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.