Add a taco-shop tutorial & launch blog post

This commit is contained in:
Matej Sima 2019-06-13 02:50:55 +02:00
parent c2d7678f9a
commit df5e88608d
18 changed files with 486 additions and 18 deletions

View File

@ -1,6 +0,0 @@
---
id: first-smart-contract
title: My first LIGO smart contract
---
TODO

View File

@ -0,0 +1,23 @@
type taco_supply is record
current_stock : nat;
max_price : tez;
end
type taco_shop_storage is map(nat, taco_supply);
function buy_taco (const taco_kind_index: nat ; var taco_shop_storage : taco_shop_storage) : (list(operation) * taco_shop_storage) is
begin
// Retrieve the taco_kind from the contract's storage
const taco_kind : taco_supply = get_force(taco_kind_index, taco_shop_storage);
const current_purchase_price : tez = taco_kind.max_price / taco_kind.current_stock;
if amount =/= current_purchase_price then
// we won't sell tacos if the amount isn't correct
fail("Sorry, the taco you're trying to purchase has a different price");
else
// Decrease the stock by 1n, because we've just sold one
taco_kind.current_stock := abs(taco_kind.current_stock - 1n);
// Update the storage with the refreshed taco_kind
taco_shop_storage[taco_kind_index] := taco_kind;
end with ((nil : list(operation)), taco_shop_storage)

View File

@ -0,0 +1,335 @@
---
id: tezos-taco-shop-smart-contract
title: Taco shop smart-contract
---
<div>
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, the **el clásico** and the **especial del chef**.
To help Pedro open his dream taco shop, we'll implement a smart-contract, that will manage supply, pricing & sales of his tacos to the consumers.
<br/>
<img src="/img/tutorials/get-started/tezos-taco-shop-smart-contract/taco-stand.svg" width="50%" />
<div style="opacity: 0.7; text-align: center; font-size: 10px;">Made by <a href="https://www.flaticon.com/authors/smashicons" title="Smashicons">Smashicons</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a> is licensed by <a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" target="_blank">CC 3.0 BY</a></div>
</div>
---
## Pricing
Pedro's tacos are a rare delicatese, so their **price goes up**, as the **stock for the day begins to deplete**.
Each taco kind, has it's own `max_price` that it sells for, and a finite supply for the current sales lifecycle.
> For the sake of simplicity, we won't implement replenishing of the supply after it runs out.
### Daily offer
|**kind** |id |**available_stock**| **max_price**|
|---|---|---|---|
|el clásico | `1n` | `50n` | `50000000mtz` |
|especial del chef | `2n` | `20n` | `75000000mtz` |
### Calculating the current purchase price
Current purchase price is calculated with the following equation:
```
current_purchase_price = max_price / available_stock
```
#### El clásico
|**available_stock**|**max_price**|**current_purchase_price**|
|---|---|---|
| `50n` | `50000000mtz` | `1tz`|
| `20n` | `50000000mtz` | `2.5tz` |
| `5n` | `50000000mtz` | `10tz` |
#### Especial del chef
|**available_stock**|**max_price**|**current_purchase_price**|
|---|---|---|
| `20n` | `75000000mtz` | `3.75tz` |
| `10n` | `75000000mtz` | `7.5tz`|
| `5n` | `75000000mtz` | `15tz` |
---
## Installing LIGO
In this tutorial, we'll use LIGO's dockerized version for the sake of simplicity. You can find the installation instructions [here](setup/installation.md#dockerized-installation-recommended).
<img src="/img/tutorials/get-started/tezos-taco-shop-smart-contract/install-ligo.png" />
<div style="opacity: 0.7; text-align: center; font-size: 12px; margin-top:-24px;">Installing the <b>next</b> version of LIGO's CLI</div>
## Implementing our first entry point
> From now on we'll get a bit more technical. If you run into something we have not covered yet - please try checking out the [LIGO cheat sheet](language-basics/cheat-sheet.md) for some extra tips & tricks.
To begin implementing our smart contract, we need an entry point. We'll call it `main` and it'll specify our contract's storage (`int`) and input parameter (`int`). Of course this is not the final storage/parameter of our contract, but it's something to get us started and test our LIGO installation as well.
### `taco-shop.ligo`
```Pascal
function main (const parameter : int; const contractStorage : int) : (list(operation) * int) is
block {skip} with ((nil : list(operation)), contractStorage + parameter)
```
Let's brake down the contract above to make sure we understand each bit of the LIGO syntax:
- **`function main`** - definition of a function that serves as an entry point
- **`(const parameter : int; const contractStorage : int)`** - parameters passed to the function
- **`const parameter : int`** - parameter provided by a transaction that invokes our contract
- **`const contractStorage : int`** - definition of our storage (`int`)
- **`(list(operation) * int)`** - return type of our function, in our case a touple with a list of operations, and an int
- **`block {skip}`** - our function has no body, so we instruct LIGO to `skip` it
- **`with ((nil : list(operation)), contractStorage + parameter)`** - essentially a return statement
- **`(nil : list(operation))`** - a `nil` value annotated as a list of operations, because that's required by our return type specified above
- **`contractStorage + 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've installed LIGO correctly, and that `taco-shop.ligo` is a valid contract, we'll dry-run it.
> Dry-running is a simulated execution of the smart contract, based on a mock storage value and a parameter.
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 entrypoint function in the contract)*
- **parameter** *(parameter to execute our contract with)*
- **storage** *(starting storage before our contract's code is executed)*
And outputs what's returned from our entrypoint - in our case a touple 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've used.
```zsh
# Contract: taco-shop.ligo
# Entry point: main
# Parameter: 4
# Storage: 3
ligo dry-run taco-shop.ligo --syntax pascaligo main 4 3
# tuple[ list[]
# 7
# ]
```
<img src="/img/tutorials/get-started/tezos-taco-shop-smart-contract/dry-run-1.png" />
<div style="opacity: 0.7; text-align: center; font-size: 12px; margin-top:-24px;">Simulating contract execution with the CLI</div>
<br/>
*`3 + 4 = 7` yay! Our CLI & contract work as expected, we can move onto fulfilling Pedro's on-chain dream.*
---
## Designing Taco shop's contract storage
We know that Pedro's Taco Shop serves two kinds of tacos, so we'll need to manage stock individually, per kind. Let's define a type, that will keep the `stock` & `max_price` per kind - in a record with two fields. Additionally, we'll want to combine our `taco_supply` type into a map, consisting of the entire offer of Pedro's shop.
**Taco shop's storage**
```Pascal
type taco_supply is record
current_stock : nat;
max_price : tez;
end
type taco_shop_storage is map(nat, taco_supply);
```
Next step is to update the `main` entry point to include `taco_shop_storage` as it's storage - while doing that let's set the `parameter` to `unit` as well to clear things up.
**`taco-shop.ligo`**
```Pascal
type taco_supply is record
current_stock : nat;
max_price : tez;
end
type taco_shop_storage is map(nat, taco_supply);
function main (const parameter: unit ; const taco_shop_storage : taco_shop_storage) : (list(operation) * taco_shop_storage) is
block {skip} with ((nil : list(operation)), taco_shop_storage)
```
### Populating our storage in a dry-run
When dry-running a contract, it's 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](tutorials/get-started/tezos-taco-shop-smart-contract.md#daily-offer), our storage's value will be defined as following:
**Storage value**
```zsh
map
1n -> record
current_stock = 50n;
max_price = 50000000mtz;
end;
2n -> record
current_stock = 20n;
max_price = 75000000mtz;
end;
end
```
> Storage value is a map, with two items in it, both items are records identified by natural numbers `1n` & `2n`.
**Dry run command with a multi-line storage value**
```zsh
ligo dry-run taco-shop.ligo --syntax pascaligo main unit "map
1n -> record
current_stock = 50n;
max_price = 50000000mtz;
end;
2n -> record
current_stock = 20n;
max_price = 75000000mtz;
end;
end"
```
<img src="/img/tutorials/get-started/tezos-taco-shop-smart-contract/dry-run-2.png" />
<div style="opacity: 0.7; text-align: center; font-size: 12px; margin-top:-24px;">Dry-run with a complex storage value</div>
<br/>
*If everything went as expected, the `dry-run` command will return the contract's current storage, which is the map of products we've defined based on the daily offer of Pedro's taco shop.*
---
## Providing an entrypoint for buying tacos
Now that we have our stock well defined in form of storage, we can move on to the actual sales. We'll replace the `main` entrypoint with `buy_taco`, that takes an `id` - effectively a key from our `taco_shop_storage` map. This will allow us to calculate pricing, and if the sale is successful - then we can reduce our stock - because we have sold a taco!
### Selling the tacos for free
Let's start by customizing our contract a bit, we will:
- rename the entrypoint from `main` to `buy_taco`
- rename `parameter` to `taco_kind_index`
- change `taco_shop_storage` to a `var` instead of a `const`, because we'll want to modify it
**`taco-shop.ligo`**
```Pascal
type taco_supply is record
current_stock : nat;
max_price : tez;
end
type taco_shop_storage is map(nat, taco_supply);
function buy_taco (const taco_kind_index: nat ; var taco_shop_storage : taco_shop_storage) : (list(operation) * taco_shop_storage) is
block { skip } with ((nil : list(operation)), taco_shop_storage)
```
#### 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 the `taco_kind_index` provided
- subtract the `taco_kind.current_stock` by `1n`
- we can find the absolute (`nat`) value of the subtraction above by using `abs`, otherwise we'd be left with an `int`
- update the storage, and return it
**`taco-shop.ligo`**
```Pascal
type taco_supply is record
current_stock : nat;
max_price : tez;
end
type taco_shop_storage is map(nat, taco_supply);
function buy_taco (const taco_kind_index: nat ; var taco_shop_storage : taco_shop_storage) : (list(operation) * taco_shop_storage) is
begin
// Retrieve the taco_kind from the contract's storage
const taco_kind : taco_supply = get_force(taco_kind_index, taco_shop_storage);
// Decrease the stock by 1n, because we've just sold one
taco_kind.current_stock := abs(taco_kind.current_stock - 1n);
// Update the storage with the refreshed taco_kind
taco_shop_storage[taco_kind_index] := taco_kind;
end with ((nil : list(operation)), taco_shop_storage)
```
<img src="/img/tutorials/get-started/tezos-taco-shop-smart-contract/dry-run-3.png" />
<div style="opacity: 0.7; text-align: center; font-size: 12px; margin-top:-24px;">Stock decreases after selling a taco</div>
<br/>
### 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 `amount`.
To make sure we get paid, we will:
- calculate a `current_purchase_price` based on the [equation specified earlier](tutorials/get-started/tezos-taco-shop-smart-contract.md#calculating-the-current-purchase-price)
- check if the sent `amount` matches the `current_purchase_price`
- if not, then our contract will `fail` and stop executing
- if yes, stock for the given `taco_kind` will be decreased and the payment accepted
**`taco-shop.ligo`**
```Pascal
type taco_supply is record
current_stock : nat;
max_price : tez;
end
type taco_shop_storage is map(nat, taco_supply);
function buy_taco (const taco_kind_index: nat ; var taco_shop_storage : taco_shop_storage) : (list(operation) * taco_shop_storage) is
begin
// Retrieve the taco_kind from the contract's storage
const taco_kind : taco_supply = get_force(taco_kind_index, taco_shop_storage);
const current_purchase_price : tez = taco_kind.max_price / taco_kind.current_stock;
if amount =/= current_purchase_price then
// we won't sell tacos if the amount isn't correct
fail("Sorry, the taco you're trying to purchase has a different price");
else
// Decrease the stock by 1n, because we've just sold one
taco_kind.current_stock := abs(taco_kind.current_stock - 1n);
// Update the storage with the refreshed taco_kind
taco_shop_storage[taco_kind_index] := taco_kind;
end with ((nil : list(operation)), taco_shop_storage)
```
In order to test the `amount` sent, we'll use the `--amount` option of `dry-run`:
```zsh
ligo dry-run taco-shop.ligo--syntax pascaligo --amount 1 buy_taco 1n "map
1n -> record
current_stock = 50n;
max_price = 50000000mtz;
end;
2n -> record
current_stock = 20n;
max_price = 75000000mtz;
end;
end"
```
**Purchasing a taco with 1.0tz**
<img src="/img/tutorials/get-started/tezos-taco-shop-smart-contract/dry-run-4.png" />
<div style="opacity: 0.7; text-align: center; font-size: 12px; margin-top:-24px;">Stock decreases after selling a taco, if the right amount of tezzies is provided</div>
<br/>
**Attempting to purchase a taco with 0.7tz**
<img src="/img/tutorials/get-started/tezos-taco-shop-smart-contract/dry-run-5.png" />
<div style="opacity: 0.7; text-align: center; font-size: 12px; margin-top:-24px;">Stock does not decrease after a purchase attempt with a lower than required amount.</div>
<br/>
**That's it - Pedro can now sell tacos on-chain, thanks to Tezos & LIGO.**
---
## 💰 Bonus: *Accepting tips above the taco purchase price*
If you'd like to accept tips in your contract as well, simply change the following line, depending on which behavior do you prefer.
**Without tips**
```Pascal
if amount =/= current_purchase_price then
```
**With tips**
```Pascal
if amount >= current_purchase_price then
```

View File

@ -1,6 +0,0 @@
---
title: Introducing LIGO
author: Matej Sima
---
Hello LIGO

View File

@ -0,0 +1,103 @@
---
title: Public Launch of LIGO
author: Gabriel Alfour
---
# Public Launch of [LIGO](https://ligolang.org/)
---
## A Refresher: What is LIGO?
LIGO is a statically typed high-level smart-contract language that compiles down to Michelson. It seeks to be easy to use, extensible and safe.
The core language is being developed by The Marigold Project. George Dupéron and Christian Rinderknecht of Nomadic Labs help on the core language, and tooling for LIGO is being developed by Stove Labs (Granary, docs and infrastructure) and Brice Aldrich (syntax highlighting).
Our previous Medium posts about LIGO can be found [here](https://medium.com/tezos/introducing-ligo-a-new-smart-contract-language-for-tezos-233fa17f21c7) and [here](https://medium.com/tezos/ligo-becomes-polyglot-a474e2cb0c24).
## The State of LIGO
Today, we are publicly releasing LIGO in beta\*. We've focused on making the onboarding process for LIGO as painless as possible and encourage you to check out our [tutorials](/docs/tutorials/get-started/tezos-taco-shop-smart-contract) and [documentation](https://ligolang.org/docs/next/setup/installation).
We are fixing bugs and adding features to LIGO (e.g. some Michelson primitives like iterators are missing) by the day. Please submit issues about bugs and missing features you need when you encounter them, and you just might find those solved in the following week.
We have been also working to extend the capabilities of Michelson, benefitting all languages (e.g. SmartPy) in the Tezos ecosystem. These proposed changes include adding multiple entrypoints, partial application (enabling cheap closures) and new operators for fast stack access to Michelson. We will submit these improvements with Nomadic Labs and Cryptium in an amendment planned for the next proposal period.
## Sample Contract
Here are two samples equivalent contracts written in two different syntaxes. They add or substract an amount to the storage depending on the parameter.
```pascal
// Pascaligo syntax
type action is
| Increment of int
| Decrement of int
function main (const p : action ; const s : int) : (list(operation) * int) is
block {skip} with ((nil : list(operation)),
case p of
| Increment n -> s + n
| Decrement n -> s - n
end)
```
```ocaml
(* Cameligo syntax *)
type action =
| Increment of int
| Decrement of int
let main (p : action) (s : int) : (operation list * int) =
let storage =
match p with
| Increment n -> s + n
| Decrement n -> s - n in
(([] : operation list) , storage)
```
## Roadmap
### Short-Term
#### June 2019
✓ First public release (hi)
✓ PascaLIGO and CameLIGO
✓ Docs
✓ Tutorials
\- Integration testing in JS/Reason with [Granary](https://stove-labs.github.io/granary/)
#### July 2019
\- Try ligo online
\- Unit testing in LIGO
\- ReasonLIGO (ReasonML syntax)
\- Design Pattern repository
### Mid-Term
We are currently planning 3 big projects on the core language (excluding tooling).
#### Generic Front End (GFE)
The PascaLIGO and CameLIGO parsers, pretty-printers and highlighters were written by hand. The same will be done for the ReasonML syntax in July.
The Generic Front End is a project to alleviate the need to do this manually for future syntaxes. The idea of the GFE is to develop a system that can take in a syntax description, and then generate:
- A parser
- A displayer
- A transpiler between syntaxes
- A syntax highlighter
- Some documentation
(A prototoype can be found in the code base that generated a PrettyPrinter, a Parser and an AST.)
#### Super Type System (STS)
The current type system is very basic: it is structural, non-polymorphic, without subtyping, names, references, advanced inference or effects. We are planning to change that.
We are looking to develop a Super Type System that has the following features:
- A rich type system. We are planning to integrate standard features (polymorphism, names), clear error messages and intuitive type inference.
- An effect system. This is important to capture failure cases, write effects in an idiomatic yet safe style (rather than passing around the storage through function calls) or capture which contracts can be called.
- An easy-to-use API. We want people to easily build static analysis tools on top of LIGO.
#### Real-time Benchmark
The current version explicitly excludes non-essential features which can produce unexpected explosions in gas costs. To alleviate this constraint, we plan to integrate gas benchmarks on all top-level declarations with some fuzzing. This will allow developers and users to estimate the cost of their contracts in real time.
## Getting Started and Contact
Come visit [our website](ligolang.org)! You can also join our [Discord](https://discord.gg/CmTwFM), Riot (*#ligo-public:matrix.org*) or Telegram Chat (Ligo Public channel).
\* Following software release cycle conventions, it should be called a pre-alpha. But most people don't know the difference.
Get started page in docs, Matej's tutorials, contributor docs, etc.

View File

@ -56,7 +56,7 @@ class Footer extends React.Component {
<div> <div>
<h5>More</h5> <h5>More</h5>
<a href={`${this.props.config.baseUrl}blog`}>Blog</a> <a href={`${this.props.config.baseUrl}blog`}>Blog</a>
<a href={this.docUrl('tutorials/first-smart-contract.html', this.props.language)}>Tutorials</a> <a href={this.docUrl('tutorials/get-started/tezos-taco-shop-smart-contract.html', this.props.language)}>Tutorials</a>
<a href={`${this.props.config.repoUrl}`}>Gitlab</a> <a href={`${this.props.config.repoUrl}`}>Gitlab</a>
</div> </div>
</section> </section>

View File

@ -16,6 +16,6 @@
"Road Map": ["contributors/road-map/short-term", "contributors/road-map/long-term"] "Road Map": ["contributors/road-map/short-term", "contributors/road-map/long-term"]
}, },
"tutorials": { "tutorials": {
"Get Started": ["tutorials/first-smart-contract"] "Get Started": ["tutorials/get-started/tezos-taco-shop-smart-contract"]
} }
} }

View File

@ -98,10 +98,10 @@ const siteConfig = {
// For no header links in the top nav bar -> headerLinks: [], // For no header links in the top nav bar -> headerLinks: [],
headerLinks: [ headerLinks: [
{doc: 'setup/installation', label: 'Docs'}, {doc: 'setup/installation', label: 'Docs'},
{doc: 'api-cli-commands', label: 'CLI'}, {doc: 'tutorials/get-started/tezos-taco-shop-smart-contract', label: 'Tutorials'},
{doc: 'tutorials/first-smart-contract', label: 'Tutorials'},
{ blog: true, label: 'Blog' }, { blog: true, label: 'Blog' },
{doc: 'contributors/origin', label: 'Contribute'}, {doc: 'contributors/origin', label: 'Contribute'},
{href: 'https://discord.gg/9rhYaEt', label: ''}
], ],
// If you have users set above, you add it here: // If you have users set above, you add it here:

View File

@ -86,7 +86,7 @@ blockquote {
} }
blockquote code { blockquote code {
opacity: 0.7; opacity: 0.5;
} }
/* /*
blockquote a { blockquote a {
@ -211,6 +211,23 @@ code {
color: #444; color: #444;
} }
body > div.fixedHeaderContainer > div > header > div > nav > ul > li:nth-child(5) {
background: url('/img/discord.svg');
background-repeat: no-repeat;
background-position: center center;
min-width: 50px;
padding-top: 5px;
opacity: 0.8;
}
body > div.fixedHeaderContainer > div > header > div > nav > ul > li:nth-child(5):hover {
opacity: 1;
}
body > div.fixedHeaderContainer > div > header > div > nav > ul > li:nth-child(5) > a:hover {
background: transparent;
}
@media only screen and (min-device-width: 360px) and (max-device-width: 736px) { @media only screen and (min-device-width: 360px) and (max-device-width: 736px) {
} }

View File

@ -0,0 +1 @@
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240"><style>.st0{fill:#FFFFFF;}</style><path class="st0" d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/><path class="st0" d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -37,7 +37,7 @@
}, },
"version-next-tutorials": { "version-next-tutorials": {
"Get Started": [ "Get Started": [
"version-next-tutorials/first-smart-contract" "version-next-tutorials/get-started/tezos-taco-shop-smart-contract"
] ]
} }
} }