diff --git a/gitlab-pages/docs/intro/ligo-intro.md b/gitlab-pages/docs/intro/ligo-intro.md index 5cdabec930..4f5fbfdcaa 100644 --- a/gitlab-pages/docs/intro/ligo-intro.md +++ b/gitlab-pages/docs/intro/ligo-intro.md @@ -98,7 +98,7 @@ For a quick overview, [get-started](../tutorials/getting-started) is a good choi Your choice to learn LIGO is already available: - Read [basics](../language-basics/types) to have a basic comprehension -- Write your first [smart contract](../tutorials/taco-shop/tezos-taco-shop-smart-contract). +- Write your first [smart contract](../tutorials/taco-shop/selling-tacos). - Others resources are available on [marigold.dev](https://www.marigold.dev/learn) ### Do you want to build a production-ready project? diff --git a/gitlab-pages/docs/tutorials/getting-started/getting-started.md b/gitlab-pages/docs/tutorials/getting-started/getting-started.md index 04b97cf553..ea1c8c2903 100644 --- a/gitlab-pages/docs/tutorials/getting-started/getting-started.md +++ b/gitlab-pages/docs/tutorials/getting-started/getting-started.md @@ -579,4 +579,4 @@ octez-client get contract storage for counter Now you have a simple LIGO smart contract and can test it, deploy it, and call it. You can use it as a starting point to write your own contracts and experiment with LIGO. -You can also continue with the [Taco shop tutorial](../taco-shop/tezos-taco-shop-smart-contract) to learn more about programming with LIGO. +You can also continue with the [Taco shop tutorial](../taco-shop/selling-tacos) to learn more about programming with LIGO. diff --git a/gitlab-pages/docs/tutorials/taco-shop/getting-payouts.md b/gitlab-pages/docs/tutorials/taco-shop/getting-payouts.md new file mode 100644 index 0000000000..4f5fb490ad --- /dev/null +++ b/gitlab-pages/docs/tutorials/taco-shop/getting-payouts.md @@ -0,0 +1,380 @@ +--- +title: "Part 3: Getting the payouts" +pagination_next: null +--- + +Now that the customer-facing entrypoint of the contract is ready, you can set up the administrator-related entrypoint. +In this case, Pedro needs a way to reset the stock of tacos and send the tez from the contract to his account. +You could do this in two entrypoints, but for simplicity this tutorial shows how to do both of these things in one entrypoint named `payout`. + +## Adding administrator information + +Also for the sake of simplicity, the contract provides no way to change Pedro's account address after the contract is deployed. +In production applications, the address of the administrator should be in the contract storage and an entrypoint should allow the current administrator to change the administrator address. +As it is, this contract cannot change the administrator address after it is deployed, so use caution. + + + +1. In the `payout` entrypoint, add this code to verify that the administrator is calling the entrypoint: + + ```jsligo skip + // Ensure that only the admin can call this entrypoint + if (Tezos.get_sender() != storage.admin_address) { + failwith("Only the admin can call this entrypoint"); + } + ``` + + The function `Tezos.get_sender` returns the address of the account that called the smart contract. + +1. Add this code to generate the operation that sends tez to the administrator account: + + ```jsligo skip + // Create contract object that represents the target account + const receiver_contract = $match(Tezos.get_contract_opt(storage.admin_address), { + "Some": (contract) => contract, + "None": () => failwith("Couldn't find account"), + }); + + // Create operation to send tez + const payout_operation = Tezos.Operation.transaction(unit, Tezos.get_balance(), receiver_contract); + ``` + + Sending tez to a user account means treating the user account as though it is a smart contract account. + This way, sending tez to a user account works in the same way as sending tez to a smart contract. + + The `Tezos.Operation.transaction` function creates a Tezos transaction. + There are many kinds of internal transactions in Tezos, but most smart contracts deal with these transactions: + + - Transferring tez to another account + - Calling an entrypoint on a smart contract + + Calling an entrypoint on a smart contract (either the current contract or another contract) is beyond the scope of this tutorial. + For information, see [Calling a contract](../../syntax/contracts/operation#calling-a-contract). + + The `Tezos.Operation.transaction` function takes these parameters: + + 1. The parameter to pass, in this case `unit`, which means no value + 1. The amount of tez to include with the transaction, in this case all of the tez the contract has, denoted by the `Tezos.get_balance` function + 1. The address of the target contract + +1. Add this code to calculate the new value of the storage, using the existing admin address and the default taco data: + + ```jsligo skip + // Restore stock of tacos + const new_storage: storage = { + admin_address: storage.admin_address, + taco_data: default_taco_data, + }; + ``` + +1. Replace the `payout` entrypoint's `return` statement with this code: + + ```jsligo skip + return [[payout_operation], new_storage]; + ``` + + Creating the transaction is not enough to run it; you must return it in the list of operations at the end of the entrypoint. + +The complete entrypoint looks like this: + +```jsligo skip +// @entry +const payout = (_u: unit, storage: storage): [ + list, + storage + ] => { + + // Ensure that only the admin can call this entrypoint + if (Tezos.get_sender() != storage.admin_address) { + failwith("Only the admin can call this entrypoint"); + } + + // Create contract object that represents the target account + const receiver_contract = $match(Tezos.get_contract_opt(storage.admin_address), { + "Some": (contract) => contract, + "None": () => failwith("Couldn't find account"), + }); + + // Create operation to send tez + const payout_operation = Tezos.Operation.transaction(unit, Tezos.get_balance(), receiver_contract); + + // Restore stock of tacos + const new_storage: storage = { + admin_address: storage.admin_address, + taco_data: default_taco_data, + }; + + return [[payout_operation], new_storage]; +} +``` + + + + + +1. In the `payout` entrypoint, add this code to verify that the administrator is calling the entrypoint: + + ```cameligo skip + (* Ensure that only the admin can call this entrypoint *) + let _ = if (Tezos.get_sender () <> storage.admin_address) then + failwith "Only the admin can call this entrypoint" in + ``` + + The function `Tezos.get_sender` returns the address of the account that called the smart contract. + +1. Add this code to generate the operation that sends tez to the administrator account: + + ```cameligo skip + (* Create contract object that represents the target account *) + let receiver_contract = match Tezos.get_contract_opt storage.admin_address with + | Some contract -> contract + | None -> failwith "Couldn't find account" in + + (* Create operation to send tez *) + let payout_operation = Tezos.Operation.transaction unit (Tezos.get_balance ()) receiver_contract in + ``` + + Sending tez to a user account means treating the user account as though it is a smart contract account. + This way, sending tez to a user account works in the same way as sending tez to a smart contract. + + The `Tezos.Operation.transaction` function creates a Tezos transaction. + There are many kinds of internal transactions in Tezos, but most smart contracts deal with these transactions: + + - Transferring tez to another account + - Calling an entrypoint on a smart contract + + Calling an entrypoint on a smart contract (either the current contract or another contract) is beyond the scope of this tutorial. + For information, see [Calling a contract](../../syntax/contracts/operation#calling-a-contract). + + The `Tezos.Operation.transaction` function takes these parameters: + + 1. The parameter to pass, in this case `unit`, which means no value + 1. The amount of tez to include with the transaction, in this case all of the tez the contract has, denoted by the `Tezos.get_balance` function + 1. The address of the target contract + +1. Add this code to calculate the new value of the storage, using the existing admin address and the default taco data: + + ```cameligo skip + (* Restore stock of tacos *) + let new_storage : storage = { + admin_address = storage.admin_address; + taco_data = default_taco_data + } in + ``` + +1. Replace the last line of the `payout` entrypoint with this code: + + ```cameligo skip + [payout_operation], new_storage + ``` + + Creating the transaction is not enough to run it; you must return it in the list of operations at the end of the entrypoint. + +The complete entrypoint looks like this: + +```cameligo skip +[@entry] +let payout (_u : unit) (storage : storage) : operation list * storage = + + (* Ensure that only the admin can call this entrypoint *) + let _ = if (Tezos.get_sender () <> storage.admin_address) then + failwith "Only the admin can call this entrypoint" in + + (* Create contract object that represents the target account *) + let receiver_contract = match Tezos.get_contract_opt storage.admin_address with + | Some contract -> contract + | None -> failwith "Couldn't find account" in + + (* Create operation to send tez *) + let payout_operation = Tezos.Operation.transaction unit (Tezos.get_balance ()) receiver_contract in + + (* Restore stock of tacos *) + let new_storage : storage = { + admin_address = storage.admin_address; + taco_data = default_taco_data + } in + + [payout_operation], new_storage +``` + + + +That's all you need to do to reset the storage and send the contract's tez to the administrator. +If you want to extend this logic, try separating the `payout` entrypoint into separate entrypoints for paying out the tez and resetting the stock of tacos. + +## Testing the new entrypoint + +Of course, after you implement the `payout` entrypoint, you should add tests for it. + + + +1. At the end of the test function, add this code to get the current balance of Pedro's account before calling the entrypoint: + + ```jsligo skip + // Test the payout entrypoint as the administrator + const admin_balance_before = Test.Address.get_balance(admin_address); + ``` + +1. Add this code to set the account that smart contract calls come from in the test scenario: + + ```jsligo skip + Test.State.set_source(admin_address); + ``` + + Now when you call the `Test.Contract.transfer` function, the transaction comes from Pedro's account. + +1. Add this code to call the `payout` entrypoint and verify that the storage was updated, as in previous tests: + + ```jsligo skip + const payout_result = + Test.Contract.transfer( + Test.Typed_address.get_entrypoint("payout", contract.taddr), + unit, + 0 as tez + ); + $match(payout_result, { + "Success": (_s) => (() => { + const storage = Test.Typed_address.get_storage(contract.taddr); + // Check that the stock has been reset + Assert.assert( + eq_in_map( + Map.find(1 as nat, TacoShop.default_taco_data), + storage.taco_data, + 1 as nat + )); + Assert.assert( + eq_in_map( + Map.find(2 as nat, TacoShop.default_taco_data), + storage.taco_data, + 2 as nat + )); + Test.IO.log("Successfully reset taco storage"); + })(), + "Fail": (_err) => failwith("Failed to reset taco storage"), + }); + ``` + +1. Add this code to verify that Pedro's account received the tez from the contract: + + ```jsligo skip + // Check that the admin account got a payout + const admin_balance_after = Test.Address.get_balance(admin_address); + Assert.assert(Test.Compare.lt(admin_balance_before, admin_balance_after)); + ``` + + The exact amounts differ because calling the `payout` entrypoint costs a small fee, but this code verifies that Pedro's account has more tez in it after calling the `payout` entrypoint. + +1. Add this code to generate a test account and verify that it can't call the `payout` entrypoint because it is not the administrator: + + ```jsligo skip + // Verify that the entrypoint fails if called by someone else + const other_user_account = Test.Account.address(1 as nat); + Test.State.set_source(other_user_account); + const failed_payout_result = + Test.Contract.transfer( + Test.Typed_address.get_entrypoint("payout", contract.taddr), + unit, + 0 as tez + ); + $match(failed_payout_result, { + "Success": (_s) => failwith("A non-admin user was able to call the payout entrypoint"), + "Fail": (_err) => Test.IO.log("Successfully prevented a non-admin user from calling the payout entrypoint"), + }); + ``` + +1. Run the test with `ligo run test taco_shop.jsligo` and verify that the test runs successfully. + + + + + +1. At the end of the test function, replace the last block with this code so the function can continue: + + ```cameligo skip + let () = match fail_result with + | Success _s -> failwith "Test was able to buy a taco for the wrong price" + | Fail _err -> Test.IO.log "Contract successfully blocked purchase with incorrect price" in + ``` + +1. Add this code to get the current balance of Pedro's account before calling the entrypoint: + + ```cameligo skip + (* Test the payout entrypoint as the administrator *) + let admin_balance_before = Test.Address.get_balance admin_address in + ``` + +1. Add this code to set the account that smart contract calls come from in the test scenario: + + ```cameligo skip + let () = Test.State.set_source admin_address in + ``` + + Now when you call the `Test.Contract.transfer` function, the transaction comes from Pedro's account. + +1. Add this code to call the `payout` entrypoint and verify that the storage was updated, as in previous tests: + + ```cameligo skip + let payout_result = Test.Contract.transfer + (Test.Typed_address.get_entrypoint "payout" contract.taddr) + unit + 0tez + in + let () = match payout_result with + | Success _s -> let storage = Test.Typed_address.get_storage contract.taddr in + let () = Assert.assert + (eq_in_map (Map.find 1n TacoShop.default_taco_data) + storage.taco_data + 1n) in + let () = Assert.assert + (eq_in_map (Map.find 2n TacoShop.default_taco_data) + storage.taco_data + 2n) in + Test.IO.log "Successfully reset taco storage" + | Fail _err -> failwith "Failed to reset taco storage" in + ``` + +1. Add this code to verify that Pedro's account received the tez from the contract: + + ```cameligo skip + (* Check that the admin account got a payout *) + let admin_balance_after = Test.Address.get_balance admin_address in + let () = Assert.assert (Test.Compare.lt admin_balance_before admin_balance_after) in + ``` + + The exact amounts differ because calling the `payout` entrypoint costs a small fee, but this code verifies that Pedro's account has more tez in it after calling the `payout` entrypoint. + +1. Add this code to generate a test account and verify that it can't call the `payout` entrypoint because it is not the administrator: + + ```cameligo skip + (* Verify that the entrypoint fails if called by someone else *) + let other_user_account = Test.Account.address 1n in + let _ = Test.State.set_source other_user_account in + let failed_payout_result = Test.Contract.transfer + (Test.Typed_address.get_entrypoint "payout" contract.taddr) + unit + 0tez + in + match failed_payout_result with + | Success _s -> failwith "A non-admin user was able to call the payout entrypoint" + | Fail _err -> Test.IO.log "Successfully prevented a non-admin user from calling the payout entrypoint" + ``` + +1. Run the test with `ligo run test taco_shop.mligo` and verify that the test runs successfully. + + + +Now you can allow different users to do different things in the contract. + +## Conclusion + +Now you have a contract that Pedro can use to sell tacos and manage the profits and the taco stock. +From here you can expand the contract in many ways, such as: + +- Adding more types of tacos +- Changing how the price of tacos is calculated +- Expanding the administrator functionality +- Accepting more than the price of the taco as a tip +- Adding more tests + +You can also try deploying the contract to a test network and trying it in a real Tezos environment. +For a tutorial that covers deploying a contract, see [Deploy a smart contract](https://docs.tezos.com/tutorials/smart-contract) on docs.tezos.com. diff --git a/gitlab-pages/docs/tutorials/taco-shop/selling-tacos.md b/gitlab-pages/docs/tutorials/taco-shop/selling-tacos.md new file mode 100644 index 0000000000..50e5ef1008 --- /dev/null +++ b/gitlab-pages/docs/tutorials/taco-shop/selling-tacos.md @@ -0,0 +1,771 @@ +--- +title: "Part 1: Creating a contract" +pagination_prev: null +--- + +import Syntax from '@theme/Syntax'; + +
+ +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](../../intro/installation). + +Optionally, you can also set up your editor to work with LIGO as described in [Editor Support](../../intro/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 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-types/records) 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: + + + +1. Anywhere on your computer, create a folder to store your work for this tutorial with a name such as `TacoShopTutorial`. + +1. 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. + +1. 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: + + ```jsligo skip + export type taco_supply = { current_stock: nat, max_price: tez }; + ``` + +1. Create a map type named `taco_data`, with the key a nat and the value the `taco_supply` type: + + ```jsligo skip + export type taco_data = map; + ``` + + This map can contain the supply and max price for any number of tacos, indexed by a natural number key. + +1. Create an address type to store Pedro's account address, which allows him to lock some features of the contract behind an administrator account: + + ```jsligo skip + export type admin_address = address; + ``` + +1. 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: + + ```jsligo skip + export type storage = { + admin_address: admin_address, + taco_data: taco_data, + }; + ``` + +1. Create a constant to represent the starting values for the taco data map: + + ```jsligo skip + 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. + +1. 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: + + ```jsligo skip + namespace TacoShop { + export type taco_supply = { current_stock: nat, max_price: tez }; + export type taco_data = map; + 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 }] + ]); + + }; + ``` + + + + + +1. Anywhere on your computer, create a folder to store your work for this tutorial with a name such as `TacoShopTutorial`. + +1. In the folder, create a file named `taco_shop.mligo` to store the code of the smart contract. +You can create and edit this file in any text editor. + +1. 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: + + ```cameligo skip + type taco_supply = { current_stock: nat; max_price: tez } + ``` + +1. Create a map type named `taco_data`, with the key a nat and the value the `taco_supply` type: + + ```cameligo skip + type taco_data = (nat, taco_supply) map + ``` + + This map can contain the supply and max price for any number of tacos, indexed by a natural number key. + +1. Create an address type to store Pedro's account address, which allows him to lock some features of the contract behind an administrator account: + + ```cameligo skip + type admin_address = address + ``` + +1. 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: + + ```cameligo skip + type storage = { + admin_address: admin_address; + taco_data: taco_data; + } + ``` + +1. Create a variable to represent the starting values for the taco data map: + + ```cameligo skip + let default_taco_data: taco_data = Map.literal [ + (1n, { current_stock = 50n; max_price = 50tez }); + (2n, { current_stock = 20n; max_price = 75tez }); + ] + ``` + + Note that the natural numbers are indicated with an `n` after the number; otherwise, LIGO assumes that numbers are integers. + Similarly, the maximum prices of the tacos are suffixed with `tez` to indicate that they are amounts of tez. + +1. To keep the code for the contract organized, put the types and values in a module named `TacoShop`. +The contract looks like this so far: + + ```cameligo skip + module TacoShop = struct + + type taco_supply = { current_stock: nat; max_price: tez } + type taco_data = (nat, taco_supply) map + type admin_address = address + type storage = { + admin_address: admin_address; + taco_data: taco_data; + } + + let default_taco_data: taco_data = Map.literal [ + (1n, { current_stock = 50n; max_price = 50tez }); + (2n, { current_stock = 20n; max_price = 75tez }); + ] + + end + ``` + + + +## 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: + +```jsligo skip +// 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; +} +``` + + + + + +Add this function inside the module, immediately after the `default_taco_data` variable: + +```cameligo skip +(* Internal function to get the price of a taco *) +let get_taco_price_internal (taco_kind_index : nat) (taco_data : taco_data) : tez = + let taco_kind : taco_supply = + match Map.find_opt taco_kind_index taco_data with + | Some kind -> kind + | None -> failwith "Unknown kind of taco" + in + 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](../../data-types/variants#options) 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](../../syntax/contracts/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: + + ```jsligo skip + // Buy a taco + // @entry + const buy_taco = (taco_kind_index: nat, storage: storage): [ + list, + 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. + +1. Within the entrypoint, add code to get the admin address and the taco data by destructuring the storage parameter: + + ```jsligo skip + const { admin_address, taco_data } = storage; + ``` + +1. After this code, add code to get the type of taco that the caller requested based on the `taco_kind_index` parameter: + + ```jsligo skip + // 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"), + }); + ``` + +1. After the code you just added, add this code to get the current price of a taco: + + ```jsligo skip + // Get the current price of this type of taco + const current_purchase_price = get_taco_price_internal(taco_kind_index, taco_data); + ``` + +1. 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: + + ```jsligo skip + // 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"); + } + ``` + +1. Add this code to verify that there is at least one taco in stock: + + ```jsligo skip + // 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"); + } + ``` + +1. Add this code to calculate the updated taco data map and put it in the `updated_taco_data` constant: + + ```jsligo skip + // 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. + +1. Create the new value of the contract storage, including the admin address and the updated taco data: + + ```jsligo skip + 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. + +1. 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: + + ```jsligo skip + // @entry + const payout = (_u: unit, storage: storage): [ + list, + storage + ] => { + + // Entrypoint logic goes here + + return [[], storage]; + } + ``` + + Currently this entrypoint does nothing, but you will add code for it later. + + + + + +- Entrypoints are functions marked with the `@entry` attribute +- 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` module, add this stub of an entrypoint: + + ```cameligo skip + (* Buy a taco *) + [@entry] + let buy_taco (taco_kind_index : nat) (storage : storage) : operation list * storage = + + (* Entrypoint logic goes here *) + + [], 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 `[], 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. + +1. Within the entrypoint, add code to get the admin address and the taco data by destructuring the storage parameter: + + ```cameligo skip + let { admin_address; taco_data } = storage in + ``` + +1. After this code, add code to get the type of taco that the caller requested based on the `taco_kind_index` parameter: + + ```cameligo skip + (* Retrieve the kind of taco from the contracts storage or fail *) + let taco_kind : taco_supply = + match Map.find_opt taco_kind_index taco_data with + | Some kind -> kind + | None -> failwith "Unknown kind of taco" in + ``` + +1. After the code you just added, add this code to get the current price of a taco: + + ```cameligo skip + (* Get the current price of this type of taco *) + let current_purchase_price = get_taco_price_internal taco_kind_index taco_data in + ``` + +1. 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: + + ```cameligo skip + (* Verify that the caller sent the correct amount of tez *) + let _ = if (Tezos.get_amount () <> current_purchase_price) then + failwith "Sorry, the taco you are trying to purchase has a different price" in + ``` + +1. Add this code to verify that there is at least one taco in stock: + + ```cameligo skip + (* Verify that there is at least one of this type of taco *) + let _ = if (taco_kind.current_stock = 0n) then + failwith "Sorry, we are out of this type of taco" in + ``` + +1. Add this code to calculate the updated taco data map and put it in the `updated_taco_data` variable: + + ```cameligo skip + (* Update the storage with the new quantity of tacos *) + let updated_taco_data : taco_data = Map.update + taco_kind_index + (Some { taco_kind with current_stock = abs (taco_kind.current_stock - 1n) }) + taco_data in + ``` + + 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. + +1. Create the new value of the contract storage, including the admin address and the updated taco data: + + ```cameligo skip + let updated_storage : storage = { + admin_address = admin_address; + taco_data = updated_taco_data; + } in + ``` + + The next line is the line `[], 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. + +1. 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: + + ```cameligo skip + [@entry] + let payout (_u : unit) (storage : storage) : operation list * storage = + + (* Entrypoint logic goes here *) + + [], 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: + +```jsligo skip +// @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](../../syntax/contracts/views). + +The complete contract file looks like this: + +```jsligo skip +namespace TacoShop { + export type taco_supply = { current_stock: nat, max_price: tez }; + export type taco_data = map; + 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, + 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, + storage + ] => { + + // Entrypoint logic goes here + + return [[], storage]; + } + +}; +``` + + + + + +Add this view to the contract, after the `get_taco_price_internal` function and somewhere within the module: + +```cameligo skip +[@view] +let 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` attribute makes external clients able to call it. + +For more information about views, see [Views](../../syntax/contracts/views). + +The complete contract file looks like this: + +```cameligo skip +module TacoShop = struct + + type taco_supply = { current_stock: nat; max_price: tez } + type taco_data = (nat, taco_supply) map + type admin_address = address + type storage = { + admin_address: admin_address; + taco_data: taco_data; + } + + let default_taco_data: taco_data = Map.literal [ + (1n, { current_stock = 50n; max_price = 50tez }); + (2n, { current_stock = 20n; max_price = 75tez }); + ] + + (* Internal function to get the price of a taco *) + let get_taco_price_internal (taco_kind_index : nat) (taco_data : taco_data) : tez = + let taco_kind : taco_supply = + match Map.find_opt taco_kind_index taco_data with + | Some kind -> kind + | None -> failwith "Unknown kind of taco" + in + taco_kind.max_price / taco_kind.current_stock + + [@view] + let get_taco_price (taco_kind_index : nat) (storage : storage) : tez = + get_taco_price_internal taco_kind_index storage.taco_data + + (* Buy a taco *) + [@entry] + let buy_taco (taco_kind_index : nat) (storage : storage) : operation list * storage = + + let { admin_address; taco_data } = storage in + + (* Retrieve the kind of taco from the contracts storage or fail *) + let taco_kind : taco_supply = + match Map.find_opt taco_kind_index taco_data with + | Some kind -> kind + | None -> failwith "Unknown kind of taco" in + + (* Get the current price of this type of taco *) + let current_purchase_price = get_taco_price_internal taco_kind_index taco_data in + + (* Verify that the caller sent the correct amount of tez *) + let _ = if (Tezos.get_amount () <> current_purchase_price) then + failwith "Sorry, the taco you are trying to purchase has a different price" in + + (* Verify that there is at least one of this type of taco *) + let _ = if (taco_kind.current_stock = 0n) then + failwith "Sorry, we are out of this type of taco" in + + + (* Update the storage with the new quantity of tacos *) + let updated_taco_data : taco_data = Map.update + taco_kind_index + (Some { taco_kind with current_stock = abs (taco_kind.current_stock - 1n) }) + taco_data in + + + let updated_storage : storage = { + admin_address = admin_address; + taco_data = updated_taco_data; + } in + + [], updated_storage + + [@entry] + let payout (_u : unit) (storage : storage) : operation list * storage = + + (* Entrypoint logic goes here *) + + [], storage + + end +``` + + + +## 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: + + + +```bash +ligo compile contract -m TacoShop -o taco_shop.tz taco_shop.jsligo +``` + + + + + +```bash +ligo compile contract -m TacoShop -o taco_shop.tz taco_shop.mligo +``` + + + +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](./testing-contract). diff --git a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/a.jsligo b/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/a.jsligo deleted file mode 100644 index f827e40d02..0000000000 --- a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/a.jsligo +++ /dev/null @@ -1,36 +0,0 @@ -namespace TacoShop { - export type taco_supply = { current_stock: nat, max_price: tez }; - export type taco_shop_storage = map; - - // @entry - function buy_taco(taco_kind_index: nat, taco_shop_storage: taco_shop_storage): [list, taco_shop_storage] { - /* Retrieve the taco_kind from the contracts storage or fail */ - const taco_kind : taco_supply = - $match(Map.find_opt (taco_kind_index, taco_shop_storage), { - "Some": kind => kind, - "None": () => failwith ("Unknown kind of taco") - }); - const current_purchase_price : tez = taco_kind.max_price / taco_kind.current_stock ; - /* We won't sell tacos if the amount is not correct */ - if ((Tezos.get_amount ()) != current_purchase_price) - return failwith ("Sorry, the taco you are trying to purchase has a different price"); - else { - /* Update the storage decreasing the stock by 1n */ - const taco_shop_storage = Map.update ( - taco_kind_index, - ["Some" as "Some", {...taco_kind, current_stock : abs (taco_kind.current_stock - (1 as nat)) }], - taco_shop_storage ); - return [[], taco_shop_storage] - } - } -}; - -const default_storage: TacoShop.taco_shop_storage = - Map.literal( - list( - [ - [1 as nat, { current_stock: 50 as nat, max_price: 50000000 as mutez }], - [2 as nat, { current_stock: 20 as nat, max_price: 75000000 as mutez }] - ] - ) - ); diff --git a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/a.mligo b/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/a.mligo deleted file mode 100644 index 43bf74f414..0000000000 --- a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/a.mligo +++ /dev/null @@ -1,53 +0,0 @@ -module TacoShop = struct - type taco_supply = - { - current_stock : nat; - max_price : tez - } - - type taco_shop_storage = (nat, taco_supply) map - - [@entry] - let buy_taco (taco_kind_index : nat) (taco_shop_storage : taco_shop_storage) - : operation list * taco_shop_storage = - (* Retrieve the taco_kind from the contract's storage or fail *) - - let taco_kind = - match Map.find_opt (taco_kind_index) taco_shop_storage with - Some k -> k - | None -> failwith "Unknown kind of taco" in - let current_purchase_price : tez = - taco_kind.max_price / taco_kind.current_stock in - (* We won't sell tacos if the amount is not correct *) - - let () = - if (Tezos.get_amount ()) <> current_purchase_price - then - failwith - "Sorry, the taco you are trying to purchase has a different price" in - (* Update the storage decreasing the stock by 1n *) - - let taco_shop_storage = - Map.update - taco_kind_index - (Some - {taco_kind with current_stock = abs (taco_kind.current_stock - 1n)}) - taco_shop_storage in - [], taco_shop_storage - -end - -let default_storage : TacoShop.taco_shop_storage = - Map.literal - [ - (1n, - { - current_stock = 50n; - max_price = 50000000mutez - }); - (2n, - { - current_stock = 20n; - max_price = 75000000mutez - }) - ] \ No newline at end of file diff --git a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/b.jsligo b/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/b.jsligo deleted file mode 100644 index 5dee19197c..0000000000 --- a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/b.jsligo +++ /dev/null @@ -1,75 +0,0 @@ -namespace TacoShop { - export type taco_supply = { current_stock: nat, max_price: tez }; - export type taco_shop_storage = map; - - const ownerAddress : address = "tz1TGu6TN5GSez2ndXXeDX6LgUDvLzPLqgYV"; - - const donationAddress : address = "tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx"; - - // @entry - function buy_taco(taco_kind_index: nat, taco_shop_storage: taco_shop_storage): [list, taco_shop_storage] { - /* Retrieve the taco_kind from the contracts storage or fail */ - const taco_kind: taco_supply = - $match(Map.find_opt(taco_kind_index, taco_shop_storage), { - "Some": kind => kind, - "None": () => failwith("Unknown kind of taco") - }); - const current_purchase_price: tez = - taco_kind.max_price / taco_kind.current_stock; - /* We won't sell tacos if the amount is not correct */ - - if ((Tezos.get_amount()) != current_purchase_price) { - return failwith( - "Sorry, the taco you are trying to purchase has a different price" - ) - } else { - /* Update the storage decreasing the stock by 1n */ - - const taco_shop_storage = - Map.update( - taco_kind_index, - [ - "Some" as "Some", { - ...taco_kind, current_stock: abs(taco_kind.current_stock - (1 as nat)) - } - ], - taco_shop_storage - ); - - const receiver : contract = - $match(Tezos.get_contract_opt (ownerAddress), { - "Some": contract => contract, - "None": () => failwith ("Not a contract") - }); - - const donationReceiver : contract = - $match((Tezos.get_contract_opt (donationAddress)), { - "Some": contract => contract, - "None": () => failwith ("Not a contract") - }); - - const donationAmount = ((Tezos.get_amount ()) / (10 as nat)) as tez; - - // Pedro will get 90% of the amount - const op1 = - $match (Tezos.get_amount () - donationAmount, { - "Some": x => Tezos.transaction (unit, x, receiver), - "None": () => failwith ("Insufficient balance") - }); - const op2 = Tezos.transaction (unit, donationAmount, donationReceiver); - const operations : list = [ op1 , op2 ]; - - return [operations, taco_shop_storage] - } - }; -} - -const default_storage: TacoShop.taco_shop_storage = - Map.literal( - list( - [ - [1 as nat, { current_stock: 50 as nat, max_price: 50000000 as mutez }], - [2 as nat, { current_stock: 20 as nat, max_price: 75000000 as mutez }] - ] - ) - ); diff --git a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/b.mligo b/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/b.mligo deleted file mode 100644 index e88bf99e20..0000000000 --- a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/b.mligo +++ /dev/null @@ -1,63 +0,0 @@ -module TacoShop = struct - type taco_supply = - { - current_stock : nat; - max_price : tez - } - - type taco_shop_storage = (nat, taco_supply) map - - let ownerAddress : address = ("tz1TGu6TN5GSez2ndXXeDX6LgUDvLzPLqgYV" : address) - - [@entry] - let buy_taco (taco_kind_index : nat) (taco_shop_storage : taco_shop_storage) - : operation list * taco_shop_storage = - (* Retrieve the taco_kind from the contract's storage or fail *) - - let taco_kind = - match Map.find_opt (taco_kind_index) taco_shop_storage with - Some k -> k - | None -> failwith "Unknown kind of taco" in - let current_purchase_price : tez = - taco_kind.max_price / taco_kind.current_stock in - (* We won't sell tacos if the amount is not correct *) - - let () = - if (Tezos.get_amount ()) <> current_purchase_price - then - failwith - "Sorry, the taco you are trying to purchase has a different price" in - (* Update the storage decreasing the stock by 1n *) - - let taco_shop_storage = - Map.update - taco_kind_index - (Some - {taco_kind with current_stock = abs (taco_kind.current_stock - 1n)}) - taco_shop_storage in - - let receiver : unit contract = - match (Tezos.get_contract_opt ownerAddress : unit contract option) with - Some (contract) -> contract - | None -> (failwith "Not a contract" : unit contract) in - - let payoutOperation : operation = Tezos.transaction () (Tezos.get_amount ()) receiver in - let operations : operation list = [payoutOperation] in - - operations, taco_shop_storage -end - -let default_storage : TacoShop.taco_shop_storage = - Map.literal - [ - (1n, - { - current_stock = 50n; - max_price = 50000000mutez - }); - (2n, - { - current_stock = 20n; - max_price = 75000000mutez - }) - ] \ No newline at end of file diff --git a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/bonus.jsligo b/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/bonus.jsligo deleted file mode 100644 index 3a1556c30e..0000000000 --- a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/bonus.jsligo +++ /dev/null @@ -1,25 +0,0 @@ -const ownerAddress : address = "tz1TGu6TN5GSez2ndXXeDX6LgUDvLzPLqgYV"; -const donationAddress : address = "tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx"; - -const receiver : contract = - $match((Tezos.get_contract_opt (ownerAddress)) as option>, { - "Some": contract => contract, - "None": () => (failwith ("Not a contract")) as contract - }); - -const donationReceiver : contract = - $match((Tezos.get_contract_opt (donationAddress)) as option>, { - "Some": contract => contract, - "None": () => (failwith ("Not a contract")) as contract - }); - -const donationAmount = ((Tezos.get_amount ()) / (10 as nat)) as tez; - -// Pedro will get 90% of the amount -const op1 = - $match((Tezos.get_amount ()) - donationAmount, { - "Some": x => Tezos.transaction (unit, x, receiver), - "None": () => failwith ("Insufficient balance") - }); -const op2 = Tezos.transaction (unit, donationAmount, donationReceiver); -const operations : list = [ op1 , op2 ]; \ No newline at end of file diff --git a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/bonus.mligo b/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/bonus.mligo deleted file mode 100644 index 5352171b6a..0000000000 --- a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/bonus.mligo +++ /dev/null @@ -1,22 +0,0 @@ -let ownerAddress : address = ("tz1TGu6TN5GSez2ndXXeDX6LgUDvLzPLqgYV" : address) -let donationAddress : address = ("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx" : address) - -let receiver : unit contract = - match ((Tezos.get_contract_opt ownerAddress) : unit contract option) with - Some contract -> contract - | None -> ((failwith "Not a contract") : unit contract) - -let donationReceiver : unit contract = - match ((Tezos.get_contract_opt donationAddress) : unit contract option) with - Some contract -> contract - | None -> ((failwith "Not a contract") : unit contract) - -let donationAmount : tez = (Tezos.get_amount ()) / 10n - -let operations : operation list = - // Pedro will get 90% of the amount - let op = match ((Tezos.get_amount ()) - donationAmount) with - | Some x -> Tezos.transaction () x receiver - | None -> (failwith "Insufficient balance") - in - [ op ; Tezos.transaction () donationAmount donationReceiver ] \ No newline at end of file diff --git a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/ex1.jsligo b/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/ex1.jsligo deleted file mode 100644 index 9300223070..0000000000 --- a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/ex1.jsligo +++ /dev/null @@ -1,9 +0,0 @@ -const ownerAddress : address = "tz1TGu6TN5GSez2ndXXeDX6LgUDvLzPLqgYV" - -const receiver : contract = - $match(Tezos.get_contract_opt(ownerAddress) as option>, { - "Some": contract => contract, - "None": () => failwith ("Not a contract") as contract - }) -const payoutOperation : operation = Tezos.transaction (unit, Tezos.get_amount (), receiver) ; -const operations : list = [payoutOperation]; \ No newline at end of file diff --git a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/ex1.mligo b/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/ex1.mligo deleted file mode 100644 index 265475f849..0000000000 --- a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-payout/ex1.mligo +++ /dev/null @@ -1,7 +0,0 @@ -let ownerAddress : address = "tz1TGu6TN5GSez2ndXXeDX6LgUDvLzPLqgYV" -let receiver : unit contract = - match (Tezos.get_contract_opt ownerAddress : unit contract option) with - Some (contract) -> contract - | None -> (failwith "Not a contract" : unit contract) -let payoutOperation : operation = Tezos.transaction () (Tezos.get_amount ()) receiver -let operations : operation list = [payoutOperation] \ No newline at end of file diff --git a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-smart-contract/TacoShop.jsligo b/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-smart-contract/TacoShop.jsligo deleted file mode 100644 index 0834787329..0000000000 --- a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-smart-contract/TacoShop.jsligo +++ /dev/null @@ -1,28 +0,0 @@ -export type taco_supply = { current_stock : nat , max_price : tez }; - -export type taco_shop_storage = map ; -const default_storage: taco_shop_storage = 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 }] -]); -// @entry -function buy_taco (taco_kind_index: nat, taco_shop_storage: taco_shop_storage) : [list, taco_shop_storage] { - /* Retrieve the taco_kind from the contracts storage or fail */ - const taco_kind : taco_supply = - $match (Map.find_opt (taco_kind_index, taco_shop_storage), { - "Some": kind => kind, - "None": () => failwith ("Unknown kind of taco") - }); - const current_purchase_price : tez = taco_kind.max_price / taco_kind.current_stock ; - /* We won't sell tacos if the amount is not correct */ - if ((Tezos.get_amount ()) != current_purchase_price) - return failwith ("Sorry, the taco you are trying to purchase has a different price"); - else { - /* Update the storage decreasing the stock by 1 nat */ - const taco_shop_storage = Map.update ( - taco_kind_index, - ["Some" as "Some", {...taco_kind, current_stock : abs (taco_kind.current_stock - (1 as nat)) }], - taco_shop_storage ); - return [[], taco_shop_storage] - } -}; \ No newline at end of file diff --git a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-smart-contract/TacoShop.mligo b/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-smart-contract/TacoShop.mligo deleted file mode 100644 index 410e62876b..0000000000 --- a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-smart-contract/TacoShop.mligo +++ /dev/null @@ -1,34 +0,0 @@ -type taco_supply = { current_stock : nat ; max_price : tez } - -type taco_shop_storage = (nat, taco_supply) map -let default_storage: taco_shop_storage = Map.literal [ - (1n, { current_stock = 50n ; max_price = 50tez }) ; - (2n, { current_stock = 20n ; max_price = 75tez }) ; -] -[@entry] -let buy_taco (taco_kind_index : nat) (taco_shop_storage : taco_shop_storage) - : operation list * taco_shop_storage = - (* Retrieve the taco_kind from the contract's storage or fail *) - - let taco_kind = - match Map.find_opt (taco_kind_index) taco_shop_storage with - Some k -> k - | None -> failwith "Unknown kind of taco" in - let current_purchase_price : tez = - taco_kind.max_price / taco_kind.current_stock in - (* We won't sell tacos if the amount is not correct *) - - let () = - if (Tezos.get_amount ()) <> current_purchase_price - then - failwith - "Sorry, the taco you are trying to purchase has a different price" in - (* Update the storage decreasing the stock by 1n *) - - let taco_shop_storage = - Map.update - taco_kind_index - (Some - {taco_kind with current_stock = abs (taco_kind.current_stock - 1n)}) - taco_shop_storage in - [], taco_shop_storage \ No newline at end of file diff --git a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-smart-contract/b12.jsligo b/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-smart-contract/b12.jsligo deleted file mode 100644 index 2fbec1a519..0000000000 --- a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-smart-contract/b12.jsligo +++ /dev/null @@ -1,9 +0,0 @@ - -namespace TacoShop { - type taco_supply = { current_stock: nat, max_price: tez }; - export type taco_shop_storage = map; - - // @entry - const buy_taco = (taco_kind_index: nat, taco_shop_storage: taco_shop_storage): [list, taco_shop_storage] => - [[], taco_shop_storage] -}; diff --git a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-smart-contract/b12.mligo b/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-smart-contract/b12.mligo deleted file mode 100644 index 21ce5f0074..0000000000 --- a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-smart-contract/b12.mligo +++ /dev/null @@ -1,14 +0,0 @@ -module TacoShop = struct - type taco_supply = - { - current_stock : nat; - max_price : tez - } - - type taco_shop_storage = (nat, taco_supply) map - - [@entry] - let buy_taco (taco_kind_index : nat) (taco_shop_storage : taco_shop_storage) : operation list * taco_shop_storage = - [], taco_shop_storage - -end \ No newline at end of file diff --git a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-smart-contract/test.jsligo b/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-smart-contract/test.jsligo deleted file mode 100644 index cc72119d77..0000000000 --- a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-smart-contract/test.jsligo +++ /dev/null @@ -1,107 +0,0 @@ -import * as TacoShop from "gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-smart-contract/TacoShop.jsligo"; - -function assert_string_failure (res: test_exec_result, expected: string) { - const expected_bis = Test.eval(expected); - $match(res, { - "Fail": x => - $match(x, { - "Rejected": y => - Assert.assert(Test.michelson_equal(y[0], expected_bis)), - "Balance_too_low": _ => - failwith("contract failed for an unknown reason"), - "Other": _o => - failwith("contract failed for an unknown reason") - }), - "Success": _s => failwith("bad price check") - }); -} - -const test = ( - (_u: unit): unit => { - /* Originate the contract with a initial storage */ - let init_storage = - Map.literal( - list( - [ - [1 as nat, { current_stock: 50 as nat, max_price: 50000000 as mutez }], - [2 as nat, { current_stock: 20 as nat, max_price: 75000000 as mutez }] - ] - ) - ); - const { addr , code , size } = - Test.originate(contract_of(TacoShop), init_storage, 0 as mutez); - - /* Test inputs */ - - const clasico_kind : parameter_of = - ["Buy_taco" as "Buy_taco", 1 as nat]; - - const unknown_kind : parameter_of = - ["Buy_taco" as "Buy_taco", 3 as nat]; - - /* Auxiliary function for testing equality in maps */ - - const eq_in_map = (r: TacoShop.taco_supply, m: TacoShop.taco_shop_storage, k: nat) => - $match(Map.find_opt(k, m), { - "None": () => false, - "Some": v => - v.current_stock == r.current_stock && v.max_price == r.max_price - }); - - /* Purchasing a Taco with 1tez and checking that the stock has been updated */ - - const ok_case: test_exec_result = - Test.transfer( - addr, - clasico_kind, - 1000000 as mutez - ); - - $match(ok_case, { - "Success": _s => - (() => { - let storage = Test.get_storage(addr); - Assert.assert( - eq_in_map( - { current_stock: 49 as nat, max_price: 50000000 as mutez }, - storage, - 1 as nat - ) - && - eq_in_map( - { current_stock: 20 as nat, max_price: 75000000 - as mutez }, - storage, - 2 as nat - ) - ); - })(), - "Fail": _e => failwith("ok test case failed") - }); - - /* Purchasing an unregistred Taco */ - - const nok_unknown_kind = - Test.transfer( - addr, - unknown_kind, - 1000000 as mutez - ); - assert_string_failure(nok_unknown_kind, "Unknown kind of taco"); - - /* Attempting to Purchase a Taco with 2tez */ - - const nok_wrong_price = - Test.transfer( - addr, - clasico_kind, - 2000000 as mutez - ); - - assert_string_failure( - nok_wrong_price, - "Sorry, the taco you are trying to purchase has a different price" - ); - return unit - } - )(); \ No newline at end of file diff --git a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-smart-contract/test.mligo b/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-smart-contract/test.mligo deleted file mode 100644 index 10a865867f..0000000000 --- a/gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-smart-contract/test.mligo +++ /dev/null @@ -1,45 +0,0 @@ -module TacoShop = Gitlab_pages.Docs.Tutorials.Taco_shop.Src.Tezos_taco_shop_smart_contract.TacoShop - -let assert_string_failure (res : test_exec_result) (expected : string) = - let expected = Test.eval expected in - match res with - | Fail (Rejected (actual,_)) -> assert (Test.michelson_equal actual expected) - | Fail _ -> failwith "contract failed for an unknown reason" - | Success _ -> failwith "bad price check" - -let test = - (* originate the contract with a initial storage *) - let init_storage = Map.literal [ - (1n, { current_stock = 50n ; max_price = 50tez }) ; - (2n, { current_stock = 20n ; max_price = 75tez }) ; ] - in - let { addr ; code = _; size = _ } = Test.originate (contract_of TacoShop) init_storage 0tez in - - (* Test inputs *) - let clasico_kind : TacoShop parameter_of = Buy_taco 1n in - let unknown_kind : TacoShop parameter_of = Buy_taco 3n in - - (* Auxiliary function for testing equality in maps *) - let eq_in_map (r : TacoShop.taco_supply) (m : TacoShop.taco_shop_storage) (k : nat) = - match Map.find_opt k m with - | None -> false - | Some v -> v.current_stock = r.current_stock && v.max_price = r.max_price in - - (* Purchasing a Taco with 1tez and checking that the stock has been updated *) - let ok_case : test_exec_result = Test.transfer addr clasico_kind 1tez in - let () = match ok_case with - | Success _ -> - let storage = Test.get_storage addr in - assert ((eq_in_map { current_stock = 49n ; max_price = 50tez } storage 1n) && - (eq_in_map { current_stock = 20n ; max_price = 75tez } storage 2n)) - | Fail _ -> failwith ("ok test case failed") - in - - (* Purchasing an unregistred Taco *) - let nok_unknown_kind = Test.transfer addr unknown_kind 1tez in - let () = assert_string_failure nok_unknown_kind "Unknown kind of taco" in - - (* Attempting to Purchase a Taco with 2tez *) - let nok_wrong_price = Test.transfer addr clasico_kind 2tez in - let () = assert_string_failure nok_wrong_price "Sorry, the taco you are trying to purchase has a different price" in - () \ No newline at end of file diff --git a/gitlab-pages/docs/tutorials/taco-shop/testing-contract.md b/gitlab-pages/docs/tutorials/taco-shop/testing-contract.md new file mode 100644 index 0000000000..0924ec43d0 --- /dev/null +++ b/gitlab-pages/docs/tutorials/taco-shop/testing-contract.md @@ -0,0 +1,513 @@ +--- +title: "Part 2: Testing the contract" +--- + +It's critical to test contracts before you deploy them because they cannot be changed after you deploy them. +LIGO includes automated testing tools that let you test contracts and verify that they work the way you intend before you deploy them. +In this section, you add tests for the `buy_taco` entrypoint. + +## Creating tests + +You can put tests in the same file as the contract or in a different file. +For convenience, in this tutorial, you put the tests in the same file. + + + +1. At the top of the contract file, outside of the namespace, add this code to import the new version of LIGO testing tools: + + ```jsligo skip + import Test = Test.Next; + import Tezos = Tezos.Next; + ``` + +1. At the end of the contract file, outside of the namespace, add this convenience function to call the view and get the current price of a taco: + + ```jsligo skip + // Convenience function to get current taco price + const get_taco_price = (untyped_address: address, taco_kind_index: nat): tez => { + const view_result_option: option = Tezos.View.call("get_taco_price", taco_kind_index, untyped_address); + return $match(view_result_option, { + "Some": (cost_mutez) => cost_mutez, + "None": () => Test.failwith("Couldn't get the price of the taco."), + }); + } + ``` + +1. Add this convenience function to verify the current stock and maximum price of a taco: + + ```jsligo skip + // Convenience function for testing equality in maps + const eq_in_map = (r: TacoShop.taco_supply, m: TacoShop.taco_data, k: nat) => + $match(Map.find_opt(k, m), { + "None": () => false, + "Some": (v) => v.current_stock == r.current_stock && v.max_price == r.max_price + }); + ``` + + This function accepts information about a taco type and verifies that the values in the stored map match. + +1. At the end of the contract file, outside of the namespace, add this stub of a function to hold the test logic: + + ```jsligo skip + const test = (() => { + + // Test logic goes here + + }) (); + ``` + +1. Inside the test function, add code to deploy the contract in the test scenario: + + ```jsligo skip + // Set the initial storage and deploy the contract + const admin_address: address = Test.Account.address(0 as nat); + const initial_storage: TacoShop.storage = { + admin_address: admin_address, + taco_data: TacoShop.default_taco_data, + } + const contract = Test.Originate.contract(contract_of(TacoShop), initial_storage, 0 as tez); + ``` + + This code creates the `contract` object to represent the deployed (originated) contract. + This object has a few fields, but the main one the test uses is the `taddr` field, which is the address of the deployed contract. + Now you can call the deployed contract in the test scenario. + +1. Get the current price of one kind of taco by calling the `get_taco_price` function: + + ```jsligo skip + // Get the current price of a taco + const untyped_address = Test.Typed_address.to_address(contract.taddr); + const current_price = get_taco_price(untyped_address, 1 as nat); + ``` + +1. Call the `buy_taco` entrypoint with this code: + + ```jsligo skip + // Purchase a taco + const success_result = + Test.Contract.transfer( + Test.Typed_address.get_entrypoint("buy_taco", contract.taddr), + 1 as nat, + current_price + ); + ``` + + The `Test.Contract.transfer` function calls an entrypoint in a test scenario. + It takes these parameters: + + 1. The contract to call, here represented by the `buy_taco` entrypoint of the contract. + 1. The parameter to pass to the entrypoint, in this case `1 as nat` to represent the first type of taco. + 1. The amount of tez to send with the transaction, in this case the current price of that type of taco from the previous lines of code. + +1. Verify that the transaction completed successfully and that the number of tacos of that type decreased by 1: + + ```jsligo skip + // Verify that the stock was updated + $match(success_result, { + "Success": (_s) => (() => { + const storage = Test.Typed_address.get_storage(contract.taddr); + // Check that the stock has been updated correctly + Assert.assert( + eq_in_map( + { current_stock: 49 as nat, max_price: 50000000 as mutez }, + storage.taco_data, + 1 as nat + )); + // Check that the amount of the other taco type has not changed + Assert.assert(eq_in_map( + { current_stock: 20 as nat, max_price: 75000000 as mutez }, + storage.taco_data, + 2 as nat + ) + ); + Test.IO.log("Successfully bought a taco"); + })(), + "Fail": (err) => failwith(err), + }); + ``` + +1. Verify that the entrypoint fails when a client passes the wrong price: + + ```jsligo skip + // Fail to purchase a taco without sending enough tez + const fail_result = + Test.Contract.transfer( + Test.Typed_address.get_entrypoint("buy_taco", contract.taddr), + 1 as nat, + 1 as mutez + ); + $match(fail_result, { + "Success": (_s) => failwith("Test was able to buy a taco for the wrong price"), + "Fail": (_err) => Test.IO.log("Contract successfully blocked purchase with incorrect price"), + }); + ``` + + It's important to test failure cases as well as success cases to make sure the contract works properly in all cases. + +The completed convenience functions and test functions look like this: + +```jsligo skip +import Test = Test.Next; +import Tezos = Tezos.Next; + +// TacoShop namespace goes here + +// Convenience function to get current taco price +const get_taco_price = (untyped_address: address, taco_kind_index: nat): tez => { + const view_result_option: option = Tezos.View.call("get_taco_price", taco_kind_index, untyped_address); + return $match(view_result_option, { + "Some": (cost_mutez) => cost_mutez, + "None": () => Test.failwith("Couldn't get the price of the taco."), + }); +} + +// Convenience function for testing equality in maps +const eq_in_map = (r: TacoShop.taco_supply, m: TacoShop.taco_data, k: nat) => + $match(Map.find_opt(k, m), { + "None": () => false, + "Some": (v) => v.current_stock == r.current_stock && v.max_price == r.max_price + }); + +const test = (() => { + + // Set the initial storage and deploy the contract + const admin_address: address = Test.Account.address(0 as nat); + const initial_storage: TacoShop.storage = { + admin_address: admin_address, + taco_data: TacoShop.default_taco_data, + } + const contract = Test.Originate.contract(contract_of(TacoShop), initial_storage, 0 as tez); + + // Get the current price of a taco + const untyped_address = Test.Typed_address.to_address(contract.taddr); + const current_price = get_taco_price(untyped_address, 1 as nat); + + // Purchase a taco + const success_result = + Test.Contract.transfer( + Test.Typed_address.get_entrypoint("buy_taco", contract.taddr), + 1 as nat, + current_price + ); + + // Verify that the stock was updated + $match(success_result, { + "Success": (_s) => (() => { + const storage = Test.Typed_address.get_storage(contract.taddr); + // Check that the stock has been updated correctly + Assert.assert( + eq_in_map( + { current_stock: 49 as nat, max_price: 50000000 as mutez }, + storage.taco_data, + 1 as nat + )); + // Check that the amount of the other taco type has not changed + Assert.assert(eq_in_map( + { current_stock: 20 as nat, max_price: 75000000 as mutez }, + storage.taco_data, + 2 as nat + ) + ); + Test.IO.log("Successfully bought a taco"); + })(), + "Fail": (err) => failwith(err), + }); + + // Fail to purchase a taco without sending enough tez + const fail_result = + Test.Contract.transfer( + Test.Typed_address.get_entrypoint("buy_taco", contract.taddr), + 1 as nat, + 1 as mutez + ); + $match(fail_result, { + "Success": (_s) => failwith("Test was able to buy a taco for the wrong price"), + "Fail": (_err) => Test.IO.log("Contract successfully blocked purchase with incorrect price"), + }); +}) (); +``` + + + + + +1. At the top of the contract file, outside of the module, add this code to import the new version of LIGO testing tools: + + ```cameligo skip + module Test = Test.Next + module Tezos = Tezos.Next + ``` + +1. At the end of the contract file, outside of the module, add this convenience function to call the view and get the current price of a taco: + + ```cameligo skip + (* Convenience function to get current taco price *) + let get_taco_price (untyped_address : address) (taco_kind_index : nat) : tez = + let view_result_option : tez option = Tezos.View.call + "get_taco_price" + taco_kind_index + untyped_address in + match view_result_option with + | Some cost_mutez -> cost_mutez + | None -> Test.failwith "Couldn't get the price of a taco" + ``` + +1. Add this convenience function to verify the current stock and maximum price of a taco: + + ```cameligo skip + (* Convenience function for testing equality in maps *) + let eq_in_map (r : TacoShop.taco_supply) (m : TacoShop.taco_data) (k : nat) = + match Map.find_opt k m with + | None -> false + | Some v -> v.current_stock = r.current_stock && v.max_price = r.max_price + ``` + + This function accepts information about a taco type and verifies that the values in the stored map match. + +1. At the end of the contract file, outside of the namespace, create a function to hold the test logic: + + ```cameligo skip + let test = + ``` + +1. Inside the test function, add code to deploy the contract in the test scenario: + + ```cameligo skip + (* Set the initial storage and deploy the contract *) + let admin_address : address = Test.Account.address 0n in + let initial_storage : TacoShop.storage = { + admin_address = admin_address; + taco_data = TacoShop.default_taco_data + } in + let contract = Test.Originate.contract (contract_of TacoShop) initial_storage 0tez in + ``` + + This code creates the `contract` object to represent the deployed (originated) contract. + This object has a few fields, but the main one the test uses is the `taddr` field, which is the address of the deployed contract. + Now you can call the deployed contract in the test scenario. + +1. Get the current price of one kind of taco by calling the `get_taco_price` function: + + ```cameligo skip + (* Get the current price of a taco *) + let untyped_address = Test.Typed_address.to_address contract.taddr in + let current_price = get_taco_price untyped_address 1n in + ``` + +1. Call the `buy_taco` entrypoint with this code: + + ```cameligo skip + (* Purchase a taco *) + let success_result = + Test.Contract.transfer + (Test.Typed_address.get_entrypoint "buy_taco" contract.taddr) + 1n + current_price + in + ``` + + The `Test.Contract.transfer` function calls an entrypoint in a test scenario. + It takes these parameters: + + 1. The contract to call, here represented by the `buy_taco` entrypoint of the contract. + 1. The parameter to pass to the entrypoint, in this case `1n` to represent the first type of taco. + 1. The amount of tez to send with the transaction, in this case the current price of that type of taco from the previous lines of code. + +1. Verify that the transaction completed successfully and that the number of tacos of that type decreased by 1: + + ```cameligo skip + (* Verify that the stock was updated *) + let () = match success_result with + | Success _s -> + let storage = Test.Typed_address.get_storage contract.taddr in + let () = Assert.assert (eq_in_map + { current_stock = 49n; max_price = 50000000mutez } + storage.taco_data + 1n + ) in + let () = Assert.assert (eq_in_map + { current_stock = 20n; max_price = 75000000mutez } + storage.taco_data + 2n + ) in + Test.IO.log "Successfully bought a taco" + | Fail err -> failwith err + in + ``` + +1. Verify that the entrypoint fails when a client passes the wrong price: + + ```cameligo skip + (* Fail to purchase a taco without sending enough tez *) + let fail_result = Test.Contract.transfer + (Test.Typed_address.get_entrypoint "buy_taco" contract.taddr) + 1n + 1mutez in + match fail_result with + | Success _s -> failwith "Test was able to buy a taco for the wrong price" + | Fail _err -> Test.IO.log "Contract successfully blocked purchase with incorrect price" + ``` + + It's important to test failure cases as well as success cases to make sure the contract works properly in all cases. + +The completed convenience functions and test functions look like this: + +```cameligo skip +module Test = Test.Next +module Tezos = Tezos.Next + +(* TacoShop module goes here *) + +(* Convenience function to get current taco price *) +let get_taco_price (untyped_address : address) (taco_kind_index : nat) : tez = + let view_result_option : tez option = Tezos.View.call + "get_taco_price" + taco_kind_index + untyped_address in + match view_result_option with + | Some cost_mutez -> cost_mutez + | None -> Test.failwith "Couldn't get the price of a taco" + +(* Convenience function for testing equality in maps *) +let eq_in_map (r : TacoShop.taco_supply) (m : TacoShop.taco_data) (k : nat) = + match Map.find_opt k m with + | None -> false + | Some v -> v.current_stock = r.current_stock && v.max_price = r.max_price + +let test = + + (* Set the initial storage and deploy the contract *) + let admin_address : address = Test.Account.address 0n in + let initial_storage : TacoShop.storage = { + admin_address = admin_address; + taco_data = TacoShop.default_taco_data + } in + let contract = Test.Originate.contract (contract_of TacoShop) initial_storage 0tez in + + (* Get the current price of a taco *) + let untyped_address = Test.Typed_address.to_address contract.taddr in + let current_price = get_taco_price untyped_address 1n in + + (* Purchase a taco *) + let success_result = + Test.Contract.transfer + (Test.Typed_address.get_entrypoint "buy_taco" contract.taddr) + 1n + current_price + in + + (* Verify that the stock was updated *) + let () = match success_result with + | Success _s -> + let storage = Test.Typed_address.get_storage contract.taddr in + let () = Assert.assert (eq_in_map + { current_stock = 49n; max_price = 50000000mutez } + storage.taco_data + 1n + ) in + let () = Assert.assert (eq_in_map + { current_stock = 20n; max_price = 75000000mutez } + storage.taco_data + 2n + ) in + Test.IO.log "Successfully bought a taco" + | Fail err -> failwith err + in + + (* Fail to purchase a taco without sending enough tez *) + let fail_result = Test.Contract.transfer + (Test.Typed_address.get_entrypoint "buy_taco" contract.taddr) + 1n + 1mutez in + match fail_result with + | Success _s -> failwith "Test was able to buy a taco for the wrong price" + | Fail _err -> Test.IO.log "Contract successfully blocked purchase with incorrect price" +``` + + + +## Running tests + +LIGO tests do not run automatically when you run the `ligo compile contract` command; you must run them with the `ligo run test` command. + +Run the tests in the contract file by running this command: + + + +```bash +ligo run test taco_shop.jsligo +``` + + + + + +```bash +ligo run test taco_shop.mligo +``` + + + +The console response prints the messages from the calls to `Test.IO.log` and a message that the test function completed: + +``` +"Successfully bought a taco" +"Contract successfully blocked purchase with incorrect price" +Everything at the top-level was executed. +- test exited with value (). +``` + +If you want to expand the tests for your contract, you can add more test functions or more test code to the existing function. +For example, you can try buying the other kind of taco or buying more of the first kind of taco and verifying that the stock and price changes as expected. + +## Testing with dry-run + +Another way to test contracts is with the `ligo run dry-run` command. +This command runs the contract in a simulated environment with parameters that you provide on the command line. +You pass these arguments to the command: + +- The contract file to run +- The amount of tez to pass with the transaction +- The parameter to pass to the contract, as a LIGO expression +- The value of the contract storage, as a LIGO expression + +For example, you can test the `buy_taco` entrypoint with this command: + + + +```bash +ligo run dry-run taco_shop.jsligo -m TacoShop --amount 1 '["Buy_taco" as "Buy_taco", 1 as nat]' \ + '{admin_address: "tz1QCVQinE8iVj1H2fckqx6oiM85CNJSK9Sx" as address, taco_data: TacoShop.default_taco_data}' +``` + +The entrypoint and parameter in this command are formatted as a variant type. +When the contract is compiled to Michelson, its parameter is a variant that has cases for each entrypoint, so you must pass the variant that corresponds to the entrypoint. +For the purposes of the `ligo run dry-run` command, the variant type is the name of the entrypoint with the first letter in upper case. +Note also that you can use variables from the contract (as in `TacoShop.default_taco_data`) in the command because the contract parameter and storage value are LIGO expressions. + + + + + +```bash +ligo run dry-run taco_shop.mligo -m TacoShop --amount 1 "Buy_taco 1n" \ + '{admin_address = "tz1QCVQinE8iVj1H2fckqx6oiM85CNJSK9Sx" ; taco_data = TacoShop.default_taco_data}' +``` + +Note that the entrypoint name starts with a capital letter when you use it in a dry run. +Note also that you can use variables from the contract (as in `TacoShop.default_taco_data`) in the command because the contract parameter and storage value are LIGO expressions. + + + +The address in the dry run command isn't stored beyond this run of the command; you just need to provide any address for the amin address in storage. +However, you must use the `as address` declaration to specify that the string is a LIGO `address` type; without the type declaration, LIGO would assume that it was a string. + +The output of the command is the return value of the entrypoint that you called. +In this case, it is an empty list of operations (`LIST_EMPTY()`) and the code for the new state of the storage. + +In this way, you can test different storage states and entrypoints from the command line. +Sometimes testing with a dry run can be more convenient than writing tests; it's up to you how to test the contract. + +Now you know that the customer interface of the contract works. +In the next section, you implement the `payout` entrypoint to retrieve the profits. +Continue to [Part 3: Getting the payouts](./getting-payouts). diff --git a/gitlab-pages/docs/tutorials/taco-shop/tezos-taco-shop-payout.md b/gitlab-pages/docs/tutorials/taco-shop/tezos-taco-shop-payout.md deleted file mode 100644 index f8e8e9bb06..0000000000 --- a/gitlab-pages/docs/tutorials/taco-shop/tezos-taco-shop-payout.md +++ /dev/null @@ -1,505 +0,0 @@ ---- -id: tezos-taco-shop-payout -title: Paying out profits from the Taco Shop ---- - -import Syntax from '@theme/Syntax'; - -In the -[previous tutorial](tezos-taco-shop-smart-contract.md) -we have learnt how to setup & interact with the LIGO CLI. Followed an -implementation of a simple Taco Shop smart contract for our -entrepreneur Pedro. - -In this tutorial we will make sure Pedro has access to tokens that -people have spent at his shop when buying tacos. - -
- - -
-
Icons made by Smashicons from www.flaticon.com is licensed by CC 3.0 BY
-
- - -## Analysing the Current Contract - - - -### **`taco-shop.mligo`** - -```cameligo group=a -module TacoShop = struct - type taco_supply = - { - current_stock : nat; - max_price : tez - } - - type taco_shop_storage = (nat, taco_supply) map - - [@entry] - let buy_taco (taco_kind_index : nat) (taco_shop_storage : taco_shop_storage) - : operation list * taco_shop_storage = - (* Retrieve the taco_kind from the contract's storage or fail *) - - let taco_kind = - match Map.find_opt (taco_kind_index) taco_shop_storage with - Some k -> k - | None -> failwith "Unknown kind of taco" in - let current_purchase_price : tez = - taco_kind.max_price / taco_kind.current_stock in - (* We won't sell tacos if the amount is not correct *) - - let () = - if (Tezos.get_amount ()) <> current_purchase_price - then - failwith - "Sorry, the taco you are trying to purchase has a different price" in - (* Update the storage decreasing the stock by 1n *) - - let taco_shop_storage = - Map.update - taco_kind_index - (Some - {taco_kind with current_stock = abs (taco_kind.current_stock - 1n)}) - taco_shop_storage in - [], taco_shop_storage - -end - -let default_storage : TacoShop.taco_shop_storage = - Map.literal - [ - (1n, - { - current_stock = 50n; - max_price = 50000000mutez - }); - (2n, - { - current_stock = 20n; - max_price = 75000000mutez - }) - ] -``` - - - - - -### **`taco-shop.jsligo`** - -```jsligo group=a -namespace TacoShop { - export type taco_supply = { current_stock: nat, max_price: tez }; - export type taco_shop_storage = map; - - // @entry - function buy_taco(taco_kind_index: nat, taco_shop_storage: taco_shop_storage): [list, taco_shop_storage] { - /* Retrieve the taco_kind from the contracts storage or fail */ - const taco_kind : taco_supply = - $match(Map.find_opt (taco_kind_index, taco_shop_storage), { - "Some": kind => kind, - "None": () => failwith ("Unknown kind of taco") - }); - const current_purchase_price : tez = taco_kind.max_price / taco_kind.current_stock ; - /* We won't sell tacos if the amount is not correct */ - if ((Tezos.get_amount ()) != current_purchase_price) - return failwith ("Sorry, the taco you are trying to purchase has a different price"); - else { - /* Update the storage decreasing the stock by 1n */ - const taco_shop_storage = Map.update ( - taco_kind_index, - ["Some" as "Some", {...taco_kind, current_stock : abs (taco_kind.current_stock - (1 as nat)) }], - taco_shop_storage ); - return [[], taco_shop_storage] - } - } -}; - -const default_storage: TacoShop.taco_shop_storage = - Map.literal( - list( - [ - [1 as nat, { current_stock: 50 as nat, max_price: 50000000 as mutez }], - [2 as nat, { current_stock: 20 as nat, max_price: 75000000 as mutez }] - ] - ) - ); - -``` - - - -### Purchase Price Formula - -Pedro's Taco Shop contract currently enables customers to buy tacos, -at a price based on a simple formula. - - - -```cameligo skip -let current_purchase_price : tez = - taco_kind.max_price / taco_kind.current_stock -``` - - - - - -```jsligo skip -const current_purchase_price : tez = - taco_kind.max_price / taco_kind.current_stock -``` - - - ---- - -## Designing a Payout Scheme - -Pedro is a standalone business owner, and in our case, he does not -have to split profits and earnings of the taco shop with anyone. So -for the sake of simplicity, we will payout all the earned XTZ directly -to Pedro right after a successful purchase. - -This means that after all the *purchase conditions* of our contract -are met, e.g., the correct amount is sent to the contract, we will not -only decrease the supply of the individual purchased *taco kind*, but -we will also transfer this amount in a *subsequent transaction* to -Pedro's personal address. - -## Forging a Payout Transaction - -### Defining the Recipient - -In order to send tokens, we will need a receiver address, which, in -our case, will be Pedro's personal account. Additionally we will wrap -the given address as a *`contract (unit)`*, which represents either a -contract with no parameters, or an implicit account. - - - -```cameligo group=ex1 -let ownerAddress : address = "tz1TGu6TN5GSez2ndXXeDX6LgUDvLzPLqgYV" -let receiver : unit contract = - match (Tezos.get_contract_opt ownerAddress : unit contract option) with - Some (contract) -> contract - | None -> (failwith "Not a contract" : unit contract) -``` - - - - - -```jsligo group=ex1 -const ownerAddress : address = "tz1TGu6TN5GSez2ndXXeDX6LgUDvLzPLqgYV" - -const receiver : contract = - $match(Tezos.get_contract_opt(ownerAddress) as option>, { - "Some": contract => contract, - "None": () => failwith ("Not a contract") as contract - }) -``` - - - -> Would you like to learn more about addresses, contracts and -> operations in LIGO? Check out the -> [LIGO cheat sheet](../../api/cheat-sheet.md) - -### Adding the Transaction to the List of Output Operations - -Now we can transfer the amount received by `buy_taco` to Pedro's -`ownerAddress`. We will do so by forging a `transaction (unit, amount, -receiver)` within a list of operations returned at the end of our -contract. - - - -```cameligo group=ex1 -let payoutOperation : operation = Tezos.transaction () (Tezos.get_amount ()) receiver -let operations : operation list = [payoutOperation] -``` - - - - - -```jsligo group=ex1 -const payoutOperation : operation = Tezos.transaction (unit, Tezos.get_amount (), receiver) ; -const operations : list = [payoutOperation]; -``` - - - ---- - -## Finalising the Contract - - - -### **`taco-shop.mligo`** - -```cameligo group=b -module TacoShop = struct - type taco_supply = - { - current_stock : nat; - max_price : tez - } - - type taco_shop_storage = (nat, taco_supply) map - - let ownerAddress : address = ("tz1TGu6TN5GSez2ndXXeDX6LgUDvLzPLqgYV" : address) - - [@entry] - let buy_taco (taco_kind_index : nat) (taco_shop_storage : taco_shop_storage) - : operation list * taco_shop_storage = - (* Retrieve the taco_kind from the contract's storage or fail *) - - let taco_kind = - match Map.find_opt (taco_kind_index) taco_shop_storage with - Some k -> k - | None -> failwith "Unknown kind of taco" in - let current_purchase_price : tez = - taco_kind.max_price / taco_kind.current_stock in - (* We won't sell tacos if the amount is not correct *) - - let () = - if (Tezos.get_amount ()) <> current_purchase_price - then - failwith - "Sorry, the taco you are trying to purchase has a different price" in - (* Update the storage decreasing the stock by 1n *) - - let taco_shop_storage = - Map.update - taco_kind_index - (Some - {taco_kind with current_stock = abs (taco_kind.current_stock - 1n)}) - taco_shop_storage in - - let receiver : unit contract = - match (Tezos.get_contract_opt ownerAddress : unit contract option) with - Some (contract) -> contract - | None -> (failwith "Not a contract" : unit contract) in - - let payoutOperation : operation = Tezos.transaction () (Tezos.get_amount ()) receiver in - let operations : operation list = [payoutOperation] in - - operations, taco_shop_storage -end - -let default_storage : TacoShop.taco_shop_storage = - Map.literal - [ - (1n, - { - current_stock = 50n; - max_price = 50000000mutez - }); - (2n, - { - current_stock = 20n; - max_price = 75000000mutez - }) - ] -``` - - - - - -### **`taco-shop.jsligo`** - -```jsligo group=b -namespace TacoShop { - export type taco_supply = { current_stock: nat, max_price: tez }; - export type taco_shop_storage = map; - - const ownerAddress : address = "tz1TGu6TN5GSez2ndXXeDX6LgUDvLzPLqgYV"; - - const donationAddress : address = "tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx"; - - // @entry - function buy_taco(taco_kind_index: nat, taco_shop_storage: taco_shop_storage): [list, taco_shop_storage] { - /* Retrieve the taco_kind from the contracts storage or fail */ - const taco_kind: taco_supply = - $match(Map.find_opt(taco_kind_index, taco_shop_storage), { - "Some": kind => kind, - "None": () => failwith("Unknown kind of taco") - }); - const current_purchase_price: tez = - taco_kind.max_price / taco_kind.current_stock; - /* We won't sell tacos if the amount is not correct */ - - if ((Tezos.get_amount()) != current_purchase_price) { - return failwith( - "Sorry, the taco you are trying to purchase has a different price" - ) - } else { - /* Update the storage decreasing the stock by 1n */ - - const taco_shop_storage = - Map.update( - taco_kind_index, - [ - "Some" as "Some", { - ...taco_kind, current_stock: abs(taco_kind.current_stock - (1 as nat)) - } - ], - taco_shop_storage - ); - - const receiver : contract = - $match(Tezos.get_contract_opt (ownerAddress), { - "Some": contract => contract, - "None": () => failwith ("Not a contract") - }); - - const donationReceiver : contract = - $match((Tezos.get_contract_opt (donationAddress)), { - "Some": contract => contract, - "None": () => failwith ("Not a contract") - }); - - const donationAmount = ((Tezos.get_amount ()) / (10 as nat)) as tez; - - // Pedro will get 90% of the amount - const op1 = - $match (Tezos.get_amount () - donationAmount, { - "Some": x => Tezos.transaction (unit, x, receiver), - "None": () => failwith ("Insufficient balance") - }); - const op2 = Tezos.transaction (unit, donationAmount, donationReceiver); - const operations : list = [ op1 , op2 ]; - - return [operations, taco_shop_storage] - } - }; -} - -const default_storage: TacoShop.taco_shop_storage = - Map.literal( - list( - [ - [1 as nat, { current_stock: 50 as nat, max_price: 50000000 as mutez }], - [2 as nat, { current_stock: 20 as nat, max_price: 75000000 as mutez }] - ] - ) - ); - -``` - - - -### Dry-run the Contract - -To confirm that our contract is valid, we can dry-run it. As a result, -we see a *new operation* in the list of returned operations to be -executed subsequently. - - - -```cameligo skip -ligo run dry-run taco-shop.mligo --syntax cameligo --amount 1 --entry-point buy_taco 1n "Map.literal [ - (1n, { current_stock = 50n; max_price = 50tez }) ; - (2n, { current_stock = 20n; max_price = 75tez }) ; -]" -``` - - - - - -```jsligo skip -ligo run dry-run taco-shop.jsligo --syntax jsligo -m TacoShop --amount 1 --entry-point buy_taco '1 as nat' "default_storage" -``` - - - - -
-Operation(...bytes) included in the output -
- -
- -**Done! Our tokens are no longer locked in the contract, and instead - they are sent to Pedro's personal account/wallet.** - ---- - -## 👼 Bonus: Donating Part of the Profits - -Because Pedro is a member of the Speciality Taco Association (STA), he -has decided to donate **10%** of the earnings to the STA. We will just -add a `donationAddress` to the contract, and compute a 10% donation -sum from each taco purchase. - - - -```cameligo group=bonus -let ownerAddress : address = ("tz1TGu6TN5GSez2ndXXeDX6LgUDvLzPLqgYV" : address) -let donationAddress : address = ("tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx" : address) - -let receiver : unit contract = - match ((Tezos.get_contract_opt ownerAddress) : unit contract option) with - Some contract -> contract - | None -> ((failwith "Not a contract") : unit contract) - -let donationReceiver : unit contract = - match ((Tezos.get_contract_opt donationAddress) : unit contract option) with - Some contract -> contract - | None -> ((failwith "Not a contract") : unit contract) - -let donationAmount : tez = (Tezos.get_amount ()) / 10n - -let operations : operation list = - // Pedro will get 90% of the amount - let op = match ((Tezos.get_amount ()) - donationAmount) with - | Some x -> Tezos.transaction () x receiver - | None -> (failwith "Insufficient balance") - in - [ op ; Tezos.transaction () donationAmount donationReceiver ] -``` - - - - - -```jsligo group=bonus -const ownerAddress : address = "tz1TGu6TN5GSez2ndXXeDX6LgUDvLzPLqgYV"; -const donationAddress : address = "tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx"; - -const receiver : contract = - $match((Tezos.get_contract_opt (ownerAddress)) as option>, { - "Some": contract => contract, - "None": () => (failwith ("Not a contract")) as contract - }); - -const donationReceiver : contract = - $match((Tezos.get_contract_opt (donationAddress)) as option>, { - "Some": contract => contract, - "None": () => (failwith ("Not a contract")) as contract - }); - -const donationAmount = ((Tezos.get_amount ()) / (10 as nat)) as tez; - -// Pedro will get 90% of the amount -const op1 = - $match((Tezos.get_amount ()) - donationAmount, { - "Some": x => Tezos.transaction (unit, x, receiver), - "None": () => failwith ("Insufficient balance") - }); -const op2 = Tezos.transaction (unit, donationAmount, donationReceiver); -const operations : list = [ op1 , op2 ]; -``` - - - -This will result into two operations being subsequently executed on the blockchain: -- Donation transfer (10%) -- Pedro's profits (90%) diff --git a/gitlab-pages/docs/tutorials/taco-shop/tezos-taco-shop-smart-contract.md b/gitlab-pages/docs/tutorials/taco-shop/tezos-taco-shop-smart-contract.md deleted file mode 100644 index a3ad20ab5a..0000000000 --- a/gitlab-pages/docs/tutorials/taco-shop/tezos-taco-shop-smart-contract.md +++ /dev/null @@ -1,649 +0,0 @@ ---- -id: tezos-taco-shop-smart-contract -title: The Taco Shop Smart Contract ---- - -import Syntax from '@theme/Syntax'; -import Link from '@docusaurus/Link'; - -
- -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. - -
- -
Made by Smashicons from www.flaticon.com is licensed by CC 3.0 BY
-
- ---- - -## 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: - -```cameligo skip -current_purchase_price = max_price / available_stock -``` - -#### 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` | - ---- -## Draft a first contract -### Designing the Taco Shop's Contract Storage - -First think to do when you create a smart contract is -think about what gonna be stored onto it. -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** - - - -```cameligo group=TacoShop -type taco_supply = { current_stock : nat ; max_price : tez } - -type taco_shop_storage = (nat, taco_supply) map -``` - - - - - -```jsligo group=TacoShop -export type taco_supply = { current_stock : nat , max_price : tez }; - -export type taco_shop_storage = map ; -``` - - - -Now that the storage is defined, let's interact with it. - -### Selling the Tacos for Free - -Create your first entrypoint `buy_taco` which is doing nothing for now : - - - -```cameligo skip -[@entry] -let buy_taco (taco_kind_index : nat) (taco_shop_storage : taco_shop_storage) : operation list * taco_shop_storage = [], taco_shop_storage - -``` - - - - - -```jsligo skip -// @entry -const buy_taco = (taco_kind_index: nat, taco_shop_storage: taco_shop_storage): [list, taco_shop_storage] => [[], taco_shop_storage] -``` - - - -It's already possible to compile your contract by running : - - - -``` -ligo compile contract taco_shop.jsligo -``` - - - - - -``` -ligo compile contract taco_shop.mligo -``` - - - -> To avoid warning at compilation, change `taco_kind_index` into `_taco_kind_index`, it'll tell to the compiler that this variable is authorized to not be used. - - -A good practice is to scope your contract into a [module](../../language-basics/modules). - - - -```cameligo group=b12 -module TacoShop = struct - type taco_supply = - { - current_stock : nat; - max_price : tez - } - - type taco_shop_storage = (nat, taco_supply) map - - [@entry] - let buy_taco (taco_kind_index : nat) (taco_shop_storage : taco_shop_storage) : operation list * taco_shop_storage = - [], taco_shop_storage - -end -``` - - - - - -```jsligo group=b12 - -namespace TacoShop { - type taco_supply = { current_stock: nat, max_price: tez }; - export type taco_shop_storage = map; - - // @entry - const buy_taco = (taco_kind_index: nat, taco_shop_storage: taco_shop_storage): [list, taco_shop_storage] => - [[], taco_shop_storage] -}; - -``` - -> We export `taco_shop_storage` to be accessible outside the module/namespace on the next section. - - - -There is an impact onto the compilation, now you have to tell to the compiler which [module](../../language-basics/modules) it need to compile : - -``` -ligo compile contract taco_shop.mligo -m TacoShop -``` - -### 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`, because the default storage is not directly used in the code, -we encourage to declare the type, if your storage mutate, your default_storage will be in error. -Reflecting [Pedro's daily offer](tezos-taco-shop-smart-contract.md#daily-offer), -our storage's value will be defined as follows: - - - -```cameligo group=TacoShop -let default_storage: taco_shop_storage = Map.literal [ - (1n, { current_stock = 50n ; max_price = 50tez }) ; - (2n, { current_stock = 20n ; max_price = 75tez }) ; -] -``` - - - - - -```jsligo group=TacoShop -const default_storage: taco_shop_storage = 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 }] -]); -``` - - - -> The storage value is a map with two bindings (entries) distinguished -> by their keys `1n` and `2n`. - -Out of curiosity, let's try to use LIGO `compile storage` command compile this value down to Michelson. - - - -```zsh -ligo compile storage TacoShop.jsligo default_storage -m TacoShop -# Output: -# -# { Elt 1 (Pair 50 50000000) ; Elt 2 (Pair 20 75000000) } -``` - - - - - -```zsh -ligo compile storage TacoShop.jsligo default_storage -m TacoShop -# Output: -# -# { Elt 1 (Pair 50 50000000) ; Elt 2 (Pair 20 75000000) } -``` - - - -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. - ---- -## Implement some logic - -### 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 value of the subtraction above by - calling `abs` (otherwise we would be left with an `int`); -- update the storage, and return it. - - - -```cameligo skip -[@entry] -let buy_taco (taco_kind_index : nat) (taco_shop_storage : taco_shop_storage) : operation list * taco_shop_storage = - (* Retrieve the taco_kind from the contract's storage or fail *) - let taco_kind = - match Map.find_opt (taco_kind_index) taco_shop_storage with - | Some k -> k - | None -> failwith "Unknown kind of taco" - in - (* Update the storage decreasing the stock by 1n *) - let taco_shop_storage = Map.update - taco_kind_index - (Some { taco_kind with current_stock = abs (taco_kind.current_stock - 1n) }) - taco_shop_storage - in - [], taco_shop_storage -``` - - - - - -```jsligo skip -// @entry -function buy_taco(taco_kind_index: nat, taco_shop_storage: taco_shop_storage): [list, taco_shop_storage] { - /* Retrieve the taco_kind from the contracts storage or fail */ - const taco_kind: taco_supply = - $match (Map.find_opt (taco_kind_index, taco_shop_storage), { - "Some": kind => kind, - "None": ()=> failwith ("Unknown kind of taco") - }); - - // Update the storage decreasing the stock by 1n - const taco_shop_storage_updated = Map.update ( - taco_kind_index, - ["Some" as "Some", {...taco_kind, - current_stock : abs(taco_kind.current_stock - (1 as nat)) }], - taco_shop_storage ); - return [[], taco_shop_storage_updated] -}; -``` - - - -### 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](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 (`failwith`) - - otherwise, stock for the given `taco_kind` will be decreased and - the payment accepted - - - -```cameligo group=TacoShop -[@entry] -let buy_taco (taco_kind_index : nat) (taco_shop_storage : taco_shop_storage) - : operation list * taco_shop_storage = - (* Retrieve the taco_kind from the contract's storage or fail *) - - let taco_kind = - match Map.find_opt (taco_kind_index) taco_shop_storage with - Some k -> k - | None -> failwith "Unknown kind of taco" in - let current_purchase_price : tez = - taco_kind.max_price / taco_kind.current_stock in - (* We won't sell tacos if the amount is not correct *) - - let () = - if (Tezos.get_amount ()) <> current_purchase_price - then - failwith - "Sorry, the taco you are trying to purchase has a different price" in - (* Update the storage decreasing the stock by 1n *) - - let taco_shop_storage = - Map.update - taco_kind_index - (Some - {taco_kind with current_stock = abs (taco_kind.current_stock - 1n)}) - taco_shop_storage in - [], taco_shop_storage -``` - - - - - -```jsligo group=TacoShop -// @entry -function buy_taco (taco_kind_index: nat, taco_shop_storage: taco_shop_storage) : [list, taco_shop_storage] { - /* Retrieve the taco_kind from the contracts storage or fail */ - const taco_kind : taco_supply = - $match (Map.find_opt (taco_kind_index, taco_shop_storage), { - "Some": kind => kind, - "None": () => failwith ("Unknown kind of taco") - }); - const current_purchase_price : tez = taco_kind.max_price / taco_kind.current_stock ; - /* We won't sell tacos if the amount is not correct */ - if ((Tezos.get_amount ()) != current_purchase_price) - return failwith ("Sorry, the taco you are trying to purchase has a different price"); - else { - /* Update the storage decreasing the stock by 1 nat */ - const taco_shop_storage = Map.update ( - taco_kind_index, - ["Some" as "Some", {...taco_kind, current_stock : abs (taco_kind.current_stock - (1 as nat)) }], - taco_shop_storage ); - return [[], taco_shop_storage] - } -}; -``` - - - -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: - - - -```cameligo test-ligo group=test -module TacoShop = Gitlab_pages.Docs.Tutorials.Taco_shop.Src.Tezos_taco_shop_smart_contract.TacoShop - -let assert_string_failure (res : test_exec_result) (expected : string) = - let expected = Test.eval expected in - match res with - | Fail (Rejected (actual,_)) -> assert (Test.michelson_equal actual expected) - | Fail _ -> failwith "contract failed for an unknown reason" - | Success _ -> failwith "bad price check" - -let test = - (* originate the contract with a initial storage *) - let init_storage = Map.literal [ - (1n, { current_stock = 50n ; max_price = 50tez }) ; - (2n, { current_stock = 20n ; max_price = 75tez }) ; ] - in - let { addr ; code = _; size = _ } = Test.originate (contract_of TacoShop) init_storage 0tez in - - (* Test inputs *) - let clasico_kind : TacoShop parameter_of = Buy_taco 1n in - let unknown_kind : TacoShop parameter_of = Buy_taco 3n in - - (* Auxiliary function for testing equality in maps *) - let eq_in_map (r : TacoShop.taco_supply) (m : TacoShop.taco_shop_storage) (k : nat) = - match Map.find_opt k m with - | None -> false - | Some v -> v.current_stock = r.current_stock && v.max_price = r.max_price in - - (* Purchasing a Taco with 1tez and checking that the stock has been updated *) - let ok_case : test_exec_result = Test.transfer addr clasico_kind 1tez in - let () = match ok_case with - | Success _ -> - let storage = Test.get_storage addr in - assert ((eq_in_map { current_stock = 49n ; max_price = 50tez } storage 1n) && - (eq_in_map { current_stock = 20n ; max_price = 75tez } storage 2n)) - | Fail _ -> failwith ("ok test case failed") - in - - (* Purchasing an unregistred Taco *) - let nok_unknown_kind = Test.transfer addr unknown_kind 1tez in - let () = assert_string_failure nok_unknown_kind "Unknown kind of taco" in - - (* Attempting to Purchase a Taco with 2tez *) - let nok_wrong_price = Test.transfer addr clasico_kind 2tez in - let () = assert_string_failure nok_wrong_price "Sorry, the taco you are trying to purchase has a different price" in - () -``` - - - - - -```jsligo test-ligo group=test -import * as TacoShop from "gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-smart-contract/TacoShop.jsligo"; - -function assert_string_failure (res: test_exec_result, expected: string) { - const expected_bis = Test.eval(expected); - $match(res, { - "Fail": x => - $match(x, { - "Rejected": y => - Assert.assert(Test.michelson_equal(y[0], expected_bis)), - "Balance_too_low": _ => - failwith("contract failed for an unknown reason"), - "Other": _o => - failwith("contract failed for an unknown reason") - }), - "Success": _s => failwith("bad price check") - }); -} - -const test = ( - (_u: unit): unit => { - /* Originate the contract with a initial storage */ - let init_storage = - Map.literal( - list( - [ - [1 as nat, { current_stock: 50 as nat, max_price: 50000000 as mutez }], - [2 as nat, { current_stock: 20 as nat, max_price: 75000000 as mutez }] - ] - ) - ); - const { addr , code , size } = - Test.originate(contract_of(TacoShop), init_storage, 0 as mutez); - - /* Test inputs */ - - const clasico_kind : parameter_of = - ["Buy_taco" as "Buy_taco", 1 as nat]; - - const unknown_kind : parameter_of = - ["Buy_taco" as "Buy_taco", 3 as nat]; - - /* Auxiliary function for testing equality in maps */ - - const eq_in_map = (r: TacoShop.taco_supply, m: TacoShop.taco_shop_storage, k: nat) => - $match(Map.find_opt(k, m), { - "None": () => false, - "Some": v => - v.current_stock == r.current_stock && v.max_price == r.max_price - }); - - /* Purchasing a Taco with 1tez and checking that the stock has been updated */ - - const ok_case: test_exec_result = - Test.transfer( - addr, - clasico_kind, - 1000000 as mutez - ); - - $match(ok_case, { - "Success": _s => - (() => { - let storage = Test.get_storage(addr); - Assert.assert( - eq_in_map( - { current_stock: 49 as nat, max_price: 50000000 as mutez }, - storage, - 1 as nat - ) - && - eq_in_map( - { current_stock: 20 as nat, max_price: 75000000 - as mutez }, - storage, - 2 as nat - ) - ); - })(), - "Fail": _e => failwith("ok test case failed") - }); - - /* Purchasing an unregistred Taco */ - - const nok_unknown_kind = - Test.transfer( - addr, - unknown_kind, - 1000000 as mutez - ); - assert_string_failure(nok_unknown_kind, "Unknown kind of taco"); - - /* Attempting to Purchase a Taco with 2tez */ - - const nok_wrong_price = - Test.transfer( - addr, - clasico_kind, - 2000000 as mutez - ); - - assert_string_failure( - nok_wrong_price, - "Sorry, the taco you are trying to purchase has a different price" - ); - return unit - } - )(); -``` - - - -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](../../reference/test.md) 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"`: - - - -```zsh -ligo run test gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-smart-contract/test.mligo -# Output: -# -# Everything at the top-level was executed. -# - test exited with value (). -``` - - - - - -```zsh -ligo run test gitlab-pages/docs/tutorials/taco-shop/src/tezos-taco-shop-smart-contract/test.jsligo -# Output: -# -# Everything at the top-level was executed. -# - test exited with value (). -``` - - - - -**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** - - - -```cameligo skip -if (Tezos.get_amount ()) <> current_purchase_price then -``` - - - - -```jsligo skip -if ((Tezos.get_amount ()) != current_purchase_price) -``` - - - -**With tips** - - - -```cameligo skip -if (Tezos.get_amount ()) >= current_purchase_price then -``` - - - - - -```jsligo skip -if ((Tezos.get_amount ()) >= current_purchase_price) -``` - - diff --git a/gitlab-pages/website/sidebars.js b/gitlab-pages/website/sidebars.js index c4bef81065..b5c376064d 100644 --- a/gitlab-pages/website/sidebars.js +++ b/gitlab-pages/website/sidebars.js @@ -12,15 +12,10 @@ const sidebars = { "intro/template", "intro/upgrade-v1", ], - "Writing a Contract": [ - { - "type": "category", - "label": "First contract", - "items": [ - "tutorials/taco-shop/tezos-taco-shop-smart-contract", - "tutorials/taco-shop/tezos-taco-shop-payout" - ] - }, + "Tutorial": [ + "tutorials/taco-shop/selling-tacos", + "tutorials/taco-shop/testing-contract", + "tutorials/taco-shop/getting-payouts", ], "Syntax": [ "syntax/comments", diff --git a/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-payout/dry-run-1.png b/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-payout/dry-run-1.png deleted file mode 100644 index a8930f63ff..0000000000 Binary files a/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-payout/dry-run-1.png and /dev/null differ diff --git a/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-payout/get-money.svg b/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-payout/get-money.svg deleted file mode 100644 index 117937d258..0000000000 --- a/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-payout/get-money.svg +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-smart-contract/dry-run-1.png b/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-smart-contract/dry-run-1.png deleted file mode 100644 index e685074fce..0000000000 Binary files a/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-smart-contract/dry-run-1.png and /dev/null differ diff --git a/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-smart-contract/dry-run-2.png b/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-smart-contract/dry-run-2.png deleted file mode 100644 index 6f5c902e0b..0000000000 Binary files a/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-smart-contract/dry-run-2.png and /dev/null differ diff --git a/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-smart-contract/dry-run-3.png b/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-smart-contract/dry-run-3.png deleted file mode 100644 index 5eb7178854..0000000000 Binary files a/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-smart-contract/dry-run-3.png and /dev/null differ diff --git a/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-smart-contract/dry-run-4.png b/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-smart-contract/dry-run-4.png deleted file mode 100644 index 6ee09b1181..0000000000 Binary files a/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-smart-contract/dry-run-4.png and /dev/null differ diff --git a/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-smart-contract/dry-run-5.png b/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-smart-contract/dry-run-5.png deleted file mode 100644 index ac390ce9cd..0000000000 Binary files a/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-smart-contract/dry-run-5.png and /dev/null differ diff --git a/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-smart-contract/install-ligo.png b/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-smart-contract/install-ligo.png deleted file mode 100644 index ca33648207..0000000000 Binary files a/gitlab-pages/website/static/img/tutorials/get-started/tezos-taco-shop-smart-contract/install-ligo.png and /dev/null differ