The Taco Shop Smart Contract
Meet Pedro, our artisan taco chef, who has decided to open a Taco shop on the Tezos blockchain, using a smart contract. He sells two different kinds of tacos: el Clásico and the Especial del Chef.
To help Pedro open his dream taco shop, we will implement a smart contract that will manage supply, pricing & sales of his tacos to the consumers.
Pricing
Pedro's tacos are a rare delicacy, so their price goes up as the stock for the day begins to deplete.
Each taco kind, has its own max_price
that it sells for, and a
finite supply for the current sales life-cycle.
For the sake of simplicity, we will not implement the replenishing of the supply after it has run out.
Daily Offer
kind | id | available_stock | max_price |
---|---|---|---|
Clásico | 1n | 50n | 50tez |
Especial del Chef | 2n | 20n | 75tez |
Calculating the Current Purchase Price
The current purchase price is calculated with the following formula:
El Clásico
available_stock | max_price | current_purchase_price |
---|---|---|
50n | 50tez | 1tez |
20n | 50tez | 2.5tez |
5n | 50tez | 10tez |
Especial del chef
available_stock | max_price | current_purchase_price |
---|---|---|
20n | 75tez | 3.75tez |
10n | 75tez | 7.5tez |
5n | 75tez | 15tez |
Installing LIGO
In this tutorial, we will use LIGO's dockerised version, for the sake of simplicity. You can find the installation instructions here.
Implementing our First main
Function
From now on we will get a bit more technical. If you run into something we have not covered yet - please try checking out the LIGO cheat sheet for some extra tips & tricks.
To begin implementing our smart contract, we need a main function,
that is the first function being executed. We will call it main
and
it will specify our contract's storage (int
) and input parameter
(int
). Of course this is not the final storage/parameter of our
contract, but it is something to get us started and test our LIGO
installation as well.
function main
- definition of the main function, which takes the parameter of the contract and the storage(const parameter : int; const contractStorage : int)
- parameters passed to the function: the first is calledparameter
because it denotes the parameter of a specific invocation of the contract, the second is the storage(list (operation) * int)
- return type of our function, in our case a tuple with a list of operations, and anint
(new value for the storage after a successful run of the contract)((nil : list (operation)), contractStorage + parameter)
- essentially a return statement(nil : list (operation))
- anil
value annotated as a list of operations, because that is required by our return type specified abovecontractStorage + parameter
- a new storage value for our contract, sum of previous storage and a transaction parameter
Running LIGO for the First Time
To test that we have installed LIGO correctly, and that
taco-shop.ligo
is a valid contract, we will dry-run it.
Dry-running is a simulated execution of the smart contract, based on a mock storage value and a parameter. We will later see a better way to test contracts: The LIGO test framework
Our contract has a storage of int
and accepts a parameter that is
also an int
.
The dry-run
command requires a few parameters:
- contract (file path)
- entrypoint (name of the main function in the contract)
- parameter (parameter to execute our contract with)
- storage (starting storage before our contract's code is executed)
It outputs what is returned from our main function: in our case a tuple containing an empty list (of operations to apply) and the new storage value, which, in our case, is the sum of the previous storage and the parameter we have used for the invocation.
3 + 4 = 7
yay! Our CLI & contract work as expected, we can move onto fulfilling Pedro's on-chain dream.
Designing the Taco Shop's Contract Storage
We know that Pedro's Taco Shop serves two kinds of tacos, so we will
need to manage stock individually, per kind. Let us define a type,
that will keep the stock
& max_price
per kind in a record with two
fields. Additionally, we will want to combine our taco_supply
type
into a map, consisting of the entire offer of Pedro's shop.
Taco shop's storage
Next step is to update the main
function to include
taco_shop_storage
in its storage. In the meanwhile, let us set the
parameter
to unit
as well to clear things up.
taco-shop.ligo
Populating our Storage
When deploying contract, it is crucial to provide a correct
initial storage value. In our case the storage is type-checked as
taco_shop_storage
. Reflecting
Pedro's daily offer,
our storage's value will be defined as follows:
The storage value is a map with two bindings (entries) distinguished by their keys
1n
and2n
.
Out of curiosity, let's try to use LIGO compile-expression
command compile this value down to Michelson.
Our initial storage record is compiled to a Michelson map { Elt 1 (Pair 50 50000000) ; Elt 2 (Pair 20 75000000) }
holding the current_stock
and max_prize
in as a pair.
Providing another Access Function for Buying Tacos
Now that we have our stock well defined in form of storage, we can
move on to the actual sales. The main
function will take a key id
from our taco_shop_storage
map and will be renamed buy_taco
for
more readability. This will allow us to calculate pricing, and if the
sale is successful, we will be able to reduce our stock because we
have sold a taco!
Selling the Tacos for Free
Let is start by customising our contract a bit, we will:
- rename
parameter
totaco_kind_index
Decreasing current_stock
when a Taco is Sold
In order to decrease the stock in our contract's storage for a specific taco kind, a few things needs to happen:
- retrieve the
taco_kind
from our storage, based on thetaco_kind_index
provided; - subtract the
taco_kind.current_stock
by1n
; - we can find the absolute value of the subtraction above by
calling
abs
(otherwise we would be left with anint
); - update the storage, and return it.
Making Sure We Get Paid for Our Tacos
In order to make Pedro's taco shop profitable, he needs to stop giving
away tacos for free. When a contract is invoked via a transaction, an
amount of tezzies to be sent can be specified as well. This amount is
accessible within LIGO as Tezos.get_amount
.
To make sure we get paid, we will:
- calculate a
current_purchase_price
based on the equation specified earlier - check if the sent amount matches the
current_purchase_price
:- if not, then our contract will fail (
failwith
) - otherwise, stock for the given
taco_kind
will be decreased and the payment accepted
- if not, then our contract will fail (
Now let's test our function against a few inputs using the LIGO test framework. For that, we will have another file in which will describe our test:
Let's break it down a little bit:
- we include the file corresponding to the smart contract we want to test;
- we define
assert_string_failure
, a function reading a transfer result and testing against a failure. It also compares the failing data - here, a string - to what we expect it to be; test
is actually performing the tests: Originates the taco-shop contract; purchasing a Taco with 1tez and checking that the stock has been updated ; attempting to purchase a Taco with 2tez and trying to purchase an unregistered Taco. An auxiliary function to check equality of values on maps is defined.
checkout the reference page for a more detailed description of the Test API
Now it is time to use the LIGO command test
. It will evaluate our
smart contract and print the result value of those entries that start
with "test"
:
The test passed ! That's it - Pedro can now sell tacos on-chain, thanks to Tezos & LIGO.
💰 Bonus: Accepting Tips above the Taco Purchase Price
If you would like to accept tips in your contract, simply change the following line, depending on your preference.
Without tips
With tips