# Tezos tutorials # Tutorials These tutorials can help you get started developing different kinds of applications on Tezos in as little as 30 minutes. import TutorialCard from '@site/src/components/TutorialCard'; import TutorialCardContainer from '@site/src/components/TutorialCardContainer'; ## Beginner These tutorials are intended for developers who are starting work with Tezos: ## Intermediate These tutorials contain multiple parts and are intended for developers with some application development experience: ## Advanced These tutorials are intended for developers who are familiar with Tezos and want to get into more powerful applications: # Deploy a smart contract This tutorial covers deploying a smart contract to Tezos. It covers how to: * Connect to a testnet * Create a wallet * Get tokens from a faucet * Code a contract, including: * Defining the storage for the contract * Defining entrypoints in the contract * Writing code to run when the entrypoints are called * Deploy (or originate) the contract to Tezos and set its starting storage value * Look up the current state of the contract * Call the contract This tutorial has different versions for different programming languages. You can run the tutorial with the version of the language you are most familiar with or want to learn. You do not need an experience in these languages to run the tutorial. * To use SmartPy, a language similar to Python, see [Deploy a smart contract with SmartPy](/tutorials/smart-contract/smartpy) * To use JsLIGO, a language similar to JavaScript and TypeScript, see [Deploy a smart contract with JsLIGO](/tutorials/smart-contract/jsligo) * To use CameLIGO, a language similar to OCaml, see [Deploy a smart contract with CameLIGO](/tutorials/smart-contract/cameligo) * To learn the Archetype language, try [Deploy a smart contract with Archetype](/tutorials/smart-contract/archetype). # Deploy a smart contract with JsLIGO Estimated time: 30 minutes This tutorial covers writing and deploying a simple smart contract with the LIGO programming language. Specifically, this tutorial uses the JsLIGO version of LIGO, which has syntax similar to JavaScript, but you don't need any experience with JavaScript or LIGO to do this tutorial. * If you are more familiar with Python, try [Deploy a smart contract with SmartPy](/tutorials/smart-contract/smartpy). * If you are more familiar with OCaml, try [Deploy a smart contract with CameLIGO](/tutorials/smart-contract/cameligo). * To learn the Archetype language, try [Deploy a smart contract with Archetype](/tutorials/smart-contract/archetype). LIGO is a high-level programming language that you can use to write smart contracts for the Tezos blockchain. It abstracts away the complexity of using Michelson (the smart contract language directly available on-chain) to make it easier to write smart contracts on Tezos. In this tutorial, you will learn how to: * Create a wallet to store cryptocurrency tokens * Get free tez tokens (the native cryptocurrency token on Tezos) from a faucet * Code a contract in LIGO, including: * Defining the storage for the contract * Defining entrypoints in the contract * Writing code to run when the entrypoints are called * Deploy (or originate) the contract to Tezos and set its starting storage value * Look up the current state of the contract * Call the contract ## What is a smart contract? A smart contract is a computer program that is stored on a blockchain and runs on a blockchain. Because the blockchain is spread across many computer nodes, you don't have to think about where to host the program or worry whether a computer will run it or not. Responsibility for running the contract is distributed across all of the nodes in the Tezos system, so when you deploy a smart contract, you can be confident that it will be available and unmodified when someone wants to run it. A smart contract has these parts: * Persistent storage, data that the contract can read and write * 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 programming languages * A Tezos account that can store tokens (technically, the contract is itself a type of Tezos account, but you can think of it as a program with a Tezos account) ## Tutorial contract The contract that you deploy in this tutorial stores a single integer. It provides entrypoints that clients can call to change the value of that integer: * The `increment` entrypoint accepts an integer as a parameter and adds that integer to the value in storage * The `decrement` entrypoint accepts an integer as a parameter and subtracts that integer from the value in storage * The `reset` entrypoint takes no parameters and resets the value in storage to 0 After you deploy the contract, you or any other user can call it from various sources, including web applications, other contracts, and the Octez command-line client. However, no one can prevent it from running or tamper with its code or its storage. ## Creating and funding a wallet To deploy and work with the contract, you need a wallet and some tez tokens. You can get test tokens for free on the Ghostnet test network: 1. Install a Tezos-compatible wallet. Which wallet you install is up to you and whether you want to install a wallet on your computer, in a browser extension, or as a mobile app. If you don't know which one to choose, try the [Temple](https://templewallet.com/) browser extension. Desktop wallets for Tezos include the [Temple](https://templewallet.com/) browser extension, [Kukai](https://wallet.kukai.app/), and [Umami](https://umamiwallet.com/). Mobile apps include [Temple](https://templewallet.com/), [Kukai](https://wallet.kukai.app/), and [Umami](https://umamiwallet.com/). 2. Switch the wallet to use the Ghostnet testnet instead of Tezos Mainnet. Ghostnet is a network for testing Tezos applications where tokens are free so you don't have to spend real currency to work with your applications. For example, for the Temple browser wallet, click **Tezos Mainnet** at the top and then click **Ghostnet Testnet**, as in this picture: ![Selecting the Ghostnet testnet in the Temple wallet](/img/tutorials/temple-switch-network.png) 3. From your wallet, get the address of your account, which starts with `tz1`. This is the address that applications use to work with your wallet. 4. Go to the Ghostnet faucet page at https://faucet.ghostnet.teztnets.com. 5. On the faucet page, paste your wallet address into the input field labeled "Or fund any address" and click the button for the amount of tez to add to your wallet. 20 tez is enough to work with the tutorial contract, and you can return to the faucet later if you need more tez. It may take a few minutes for the faucet to send the tokens and for those tokens to appear in your wallet. You can use the faucet as much as you need to get tokens on the testnet, but those tokens are worthless and cannot be used on Mainnet. ![Fund your wallet using the Ghostnet Faucet](/img/tutorials/wallet-funding.png) Now you have an account and funds that you can use to work with Tezos. ## Creating the contract The contract that you will create has these basic parts: * A type that describes the contract's storage, in this case an integer. The storage can be a primitive type such as an integer, string, or timestamp, or a complex data type that contains multiple values. For more information on contract data types, see [Data types](/smart-contracts/data-types). * Functions called entrypoints that run code when clients call the contract. * A type that describes the return value of the entrypoints. Follow these steps to create the code for the contract: 1. Open the LIGO online IDE at https://ide.ligolang.org/. You can work with LIGO code in any IDE, but this online IDE keeps you from having to install software on your computer, and it also simplifies the process of deploying contracts. 2. At the top right of the page, in the **Network** menu, select Ghostnet, as shown in this picture: ![Selecting Ghostnet in the list of networks](/img/tutorials/ligo-online-ide-select-ghostnet.png) 3. Connect a wallet to the IDE: 1. At the top right of the page, click the **Keypair Manager** button. 2. In the Keypair Manager window, import the account that you created earlier or create and fund a new account to use with the IDE. * To import the account that you created earlier, export the private key from your wallet app, click **Import** in the Keypair Manager window, and paste the private key. Now you can use your account in the IDE. * To create an account to use with the IDE, click **Create** in the Keypair Manager window, give the new keypair a name, and click **Create**. Then, copy the address of the keypair and get tez from the faucet as you did in [Creating and funding a wallet](#creating-and-funding-a-wallet). 4. In the IDE, create a project from the empty template and select the JsLIGO syntax, as shown in this picture: Creating a project The IDE creates a project and a contract file named `Contract.jsligo`. 5. In the contract file, create a namespace named `Counter` to hold the code for the contract: ```jsligo namespace Counter { } ``` 6. Inside the namespace, create a TypeScript type to set the storage type to an integer: ```jsligo type storage = int; ``` 7. Add this code to define the return type for the entrypoints. Tezos entrypoints return two values: a list of other operations to call and the new value of the contract's storage. ```jsligo type returnValue = [list, storage]; ``` 8. Add the code for the increment and decrement entrypoints: ```jsligo // Increment entrypoint @entry const increment = (delta : int, store : storage) : returnValue => [list([]), store + delta]; // Decrement entrypoint @entry const decrement = (delta : int, store : storage) : returnValue => [list([]), store - delta]; ``` These functions begin with the `@entry` annotation to indicate that they are entrypoints. They accept two parameters: the change in the storage value (an integer) and the current value of the storage (in the `storage` type that you created earlier in the code). They return a value of the type `returnValue` that you created in the previous step. Each function returns an empty list of other operations to call and the new value of the storage. 9. Add this code for the reset entrypoint: ```jsligo // Reset entrypoint @entry const reset = (_p : unit, _s : storage) : returnValue => [list([]), 0]; ``` This function is similar to the others, but it does not take the current value of the storage into account. It always returns an empty list of operations and 0. The complete contract code looks like this: ```jsligo namespace Counter { type storage = int; type returnValue = [list, storage]; // Increment entrypoint @entry const increment = (delta : int, store : storage) : returnValue => [list([]), store + delta]; // Decrement entrypoint @entry const decrement = (delta : int, store : storage) : returnValue => [list([]), store - delta]; // Reset entrypoint @entry const reset = (_p : unit, _s : storage) : returnValue => [list([]), 0]; } ``` ## Testing and compiling the contract Before you can deploy the contract to Tezos, you must compile it to Michelson, the base language of Tezos contracts. 1. Set the compiler to target the namespace to compile in your code: 1. On the left side of the page, under **Actions**, click **Project Settings**. 2. On the Project Settings tab, in the **Module name** field, set the module name to `Counter`. 3. Close the Project Settings tab. 2. Test the contract by passing parameters and the storage value to the LIGO `dry-run` command: 1. On the left side of the page, under **Actions**, click **Dry Run**. 2. In the Dry Run window, select the `Increment` entrypoint, set the input parameter to `32` and the storage to `10`. The Dry Run window looks like this: The Dry Run window, showing the entrypoint to run, the parameter to pass, and the value of the storage 3. Click **Run**. At the bottom of the window, the Result field shows the response `(LIST_EMPTY(), 42)`. This response means that the contract did not call any other contracts, so the list of operations is empty. Then it shows the new value of the storage. You can test the decrement function in the same way. If you see any errors, make sure that the code of your contract matches the code in the previous section. 4. Test the `Reset` entrypoint in the same way, but pass `unit` as the input parameter and any integer in the storage field. The `Reset` entrypoint takes no parameters, but technically it accepts the value `unit`, which means no parameter. The Result field shows the response `(LIST_EMPTY(), 0)`, which means that the storage value is now 0. 3. On the left side of the page, under **Actions**, click **Compile**, and in the Compile window, click **Compile** again. If the compilation succeeds, the IDE prints the compiled code to the terminal and saves it to the file `build/contracts/Contract.tz`. You can see the code by expanding your project on the left side of the page, under `.workspaces`, and double-clicking `Contract.tz`. If you see error messages, verify that your contract code matches the code in the previous section. Now you can deploy the contract. ## Deploying (originating) to the testnet Deploying a contract to the network is called "originating." Originating the contract requires a small amount of Tezos tokens as a fee. 1. On the left side of the page, under **Actions**, click **Deploy**. You may see a warning that the initial storage is not set. You can ignore this warning because you can set the initial storage now. 2. In the Deploy contract window, in the **Init storage** field, set the initial value for the contract's storage to an integer. 3. In the **Signer** field, make sure your account is selected. 4. Click **Estimate**. The window shows the estimated fees to deploy the contract, as in this picture: The estimate of the fees to deploy the contract 5. Click **Deploy**. The deployment process can take a few minutes. When the contract is deployed, the Deploy contract window shows the address at the bottom of the window. 6. Copy the address of the deployed contract, which starts with `KT1`. :::warning Copy the contract address now, because it will not be shown again. ::: Now you can call the contract from any Tezos client, including web applications and command-line applications like [The Octez client](/developing/octez-client). ## Calling the contract These steps show you how to inspect the contract with a block explorer, which is a web application that shows information about Tezos. It also allows you to call the contract. 1. Open the block explorer Better Call Dev at this link: https://better-call.dev/ 2. Paste the address of the contract in the search field and press Enter. The block explorer shows information about the contract, including recent transactions and the current state of its storage. The block explorer, showing information about the contract 3. Try calling one of the entrypoints: 1. Go to the **Storage** tab and check the current state of the storage, which should be the integer that you put in the Deploy window. 2. Go to the **Interact** tab. This tab shows the entrypoints in the contract and lets you use them. 3. For the `increment` entrypoint, in the **Parameters** section, put an integer in the field, as shown in this image: Putting in a value for an entrypoint parameter 4. Click **Execute** and then click **Wallet**. 5. Select your wallet and connect it to the application. 6. Confirm the transaction in your wallet. 7. Wait for a success message that says "The transaction has successfully been broadcasted to the network." 8. Go back to the **Storage** tab and see the new value of the storage, as in this picture: Updated storage value ## Summary Now the contract is running on the Tezos blockchain. You or any other user can call it from any source that can send transactions to Tezos, including Octez, dApps, and other contracts. If you want to continue working with this contract, here are some ideas: * Change permissions for the contract so only your account can call its entrypoints * Add your own entrypoints and originate a new contract; note that you cannot update the existing contract after it is deployed * Create a dApp to call the contract from a web application, similar to the dApp that you create in the tutorial [Build a simple web application](/tutorials/build-your-first-app/) # Deploy a smart contract with CameLIGO Estimated time: 30 minutes This tutorial covers writing and deploying a simple smart contract with the LIGO programming language. Specifically, this tutorial uses the CameLIGO version of LIGO, which has syntax similar to OCaml, but you don't need any experience with OCaml or LIGO to do this tutorial. * If you are more familiar with JavaScript, try [Deploy a smart contract with JsLIGO](/tutorials/smart-contract/jsligo). * If you are more familiar with Python, try [Deploy a smart contract with SmartPy](/tutorials/smart-contract/smartpy). * To learn the Archetype language, try [Deploy a smart contract with Archetype](/tutorials/smart-contract/archetype). LIGO is a high-level programming language that you can use to write smart contracts for the Tezos blockchain. It abstracts away the complexity of using Michelson (the smart contract language directly available on-chain) to make it easier to write smart contracts on Tezos. In this tutorial, you will learn how to: * Create a wallet to store cryptocurrency tokens * Get free tez tokens (the native cryptocurrency token on Tezos) from a faucet * Code a contract in LIGO, including: * Defining the storage for the contract * Defining entrypoints in the contract * Writing code to run when the entrypoints are called * Deploy (or originate) the contract to Tezos and set its starting storage value * Look up the current state of the contract * Call the contract ## What is a smart contract? A smart contract is a computer program that is stored on a blockchain and runs on a blockchain. Because the blockchain is spread across many computer nodes, you don't have to think about where to host the program or worry whether a computer will run it or not. Responsibility for running the contract is distributed across all of the nodes in the Tezos system, so when you deploy a smart contract, you can be confident that it will be available and unmodified when someone wants to run it. A smart contract has these parts: * Persistent storage, data that the contract can read and write * 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 programming languages * A Tezos account that can store tokens (technically, the contract is itself a type of Tezos account, but you can think of it as a program with a Tezos account) ## Tutorial contract The contract that you deploy in this tutorial stores a single integer. It provides entrypoints that clients can call to change the value of that integer: * The `increment` entrypoint accepts an integer as a parameter and adds that integer to the value in storage * The `decrement` entrypoint accepts an integer as a parameter and subtracts that integer from the value in storage * The `reset` entrypoint takes no parameters and resets the value in storage to 0 After you deploy the contract, you or any other user can call it from various sources, including web applications, other contracts, and the Octez command-line client. However, no one can prevent it from running or tamper with its code or its storage. ## Creating and funding a wallet To deploy and work with the contract, you need a wallet and some tez tokens. You can get test tokens for free on the Ghostnet test network: 1. Install a Tezos-compatible wallet. Which wallet you install is up to you and whether you want to install a wallet on your computer, in a browser extension, or as a mobile app. If you don't know which one to choose, try the [Temple](https://templewallet.com/) browser extension. Desktop wallets for Tezos include the [Temple](https://templewallet.com/) browser extension, [Kukai](https://wallet.kukai.app/), and [Umami](https://umamiwallet.com/). Mobile apps include [Temple](https://templewallet.com/), [Kukai](https://wallet.kukai.app/), and [Umami](https://umamiwallet.com/). 2. Switch the wallet to use the Ghostnet testnet instead of Tezos Mainnet. Ghostnet is a network for testing Tezos applications where tokens are free so you don't have to spend real currency to work with your applications. For example, for the Temple browser wallet, click **Tezos Mainnet** at the top and then click **Ghostnet Testnet**, as in this picture: ![Selecting the Ghostnet testnet in the Temple wallet](/img/tutorials/temple-switch-network.png) 3. From your wallet, get the address of your account, which starts with `tz1`. This is the address that applications use to work with your wallet. 4. Go to the Ghostnet faucet page at https://faucet.ghostnet.teztnets.com. 5. On the faucet page, paste your wallet address into the input field labeled "Or fund any address" and click the button for the amount of tez to add to your wallet. 20 tez is enough to work with the tutorial contract, and you can return to the faucet later if you need more tez. It may take a few minutes for the faucet to send the tokens and for those tokens to appear in your wallet. You can use the faucet as much as you need to get tokens on the testnet, but those tokens are worthless and cannot be used on Mainnet. ![Fund your wallet using the Ghostnet Faucet](/img/tutorials/wallet-funding.png) Now you have an account and funds that you can use to work with Tezos. ## Creating the contract The contract that you will create has these basic parts: * A type that describes the contract's storage, in this case an integer. The storage can be a primitive type such as an integer, string, or timestamp, or a complex data type that contains multiple values. For more information on contract data types, see [Data types](/smart-contracts/data-types). * Functions called entrypoints that run code when clients call the contract. * A type that describes the return value of the entrypoints. Follow these steps to create the code for the contract: 1. Open the LIGO online IDE at https://ide.ligolang.org/. You can work with LIGO code in any IDE, but this online IDE keeps you from having to install software on your computer, and it also simplifies the process of deploying contracts. 2. At the top right of the page, in the **Network** menu, select Ghostnet, as shown in this picture: ![Selecting Ghostnet in the list of networks](/img/tutorials/ligo-online-ide-select-ghostnet.png) 3. Connect a wallet to the IDE: 1. At the top right of the page, click the **Keypair Manager** button. 2. In the Keypair Manager window, import the account that you created earlier or create and fund a new account to use with the IDE. * To import the account that you created earlier, export the private key from your wallet app, click **Import** in the Keypair Manager window, and paste the private key. Now you can use your account in the IDE. * To create an account to use with the IDE, click **Create** in the Keypair Manager window, give the new keypair a name, and click **Create**. Then, copy the address of the keypair and get tez from the faucet as you did in [Creating and funding a wallet](#creating-and-funding-a-wallet). 4. In the IDE, create a project from the empty template and select the CameLIGO syntax, as shown in this picture: Creating a project The IDE creates a project and a contract file named `Contract.mligo`. 5. In the contract file, create a type to set the storage type to an integer: ```cameligo type storage = int ``` 6. Add this code to define the return type for the entrypoints. Tezos entrypoints return two values: a list of other operations to call and the new value of the contract's storage. ```cameligo type returnValue = operation list * storage ``` 7. Add the code for the increment and decrement entrypoints: ```cameligo // Increment entrypoint [@entry] let increment (delta : int) (store : storage) : returnValue = [], store + delta // Decrement entrypoint [@entry] let decrement (delta : int) (store : storage) : returnValue = [], store - delta ``` These functions begin with the `@entry` annotation to indicate that they are entrypoints. They accept two parameters: the change in the storage value (an integer) and the current value of the storage (in the `storage` type that you created earlier in the code). They return a value of the type `returnValue` that you created in the previous step. Each function returns an empty list of other operations to call and the new value of the storage. 8. Add this code for the reset entrypoint: ```cameligo // Reset entrypoint [@entry] let reset (() : unit) (_ : storage) : returnValue = [], 0 ``` This function is similar to the others, but it does not take the current value of the storage into account. It always returns an empty list of operations and 0. The complete contract code looks like this: ```cameligo type storage = int type returnValue = operation list * storage // Increment entrypoint [@entry] let increment (delta : int) (store : storage) : returnValue = [], store + delta // Decrement entrypoint [@entry] let decrement (delta : int) (store : storage) : returnValue = [], store - delta // Reset entrypoint [@entry] let reset (() : unit) (_ : storage) : returnValue = [], 0 ``` ## Testing and compiling the contract Before you can deploy the contract to Tezos, you must compile it to Michelson, the base language of Tezos contracts. 1. Set the compiler to target the module to compile in your code: 1. On the left side of the page, under **Actions**, click **Project Settings**. 2. On the Project Settings tab, in the **Module name** field, set the module name to `Counter`. 3. Close the Project Settings tab. 2. Test the contract by passing parameters and the storage value to the LIGO `dry-run` command: 1. On the left side of the page, under **Actions**, click **Dry Run**. 2. In the Dry Run window, select the `Increment` entrypoint, set the input parameter to `32` and the storage to `10`. The Dry Run window looks like this: The Dry Run window, showing the entrypoint to run, the parameter to pass, and the value of the storage 3. Click **Run**. At the bottom of the window, the Result field shows the response `(LIST_EMPTY(), 42)`. This response means that the contract did not call any other contracts, so the list of operations is empty. Then it shows the new value of the storage. You can test the decrement function in the same way. If you see any errors, make sure that the code of your contract matches the code in the previous section. 4. Test the `Reset` entrypoint in the same way, but pass `unit` as the input parameter and any integer in the storage field. The `Reset` entrypoint takes no parameters, but technically it accepts the value `unit`, which means no parameter. The Result field shows the response `(LIST_EMPTY(), 0)`, which means that the storage value is now 0. 3. On the left side of the page, under **Actions**, click **Compile**, and in the Compile window, click **Compile** again. If the compilation succeeds, the IDE prints the compiled code to the terminal and saves it to the file `build/contracts/Contract.tz`. You can see the code by expanding your project on the left side of the page, under `.workspaces`, and double-clicking `Contract.tz`. If you see error messages, verify that your contract code matches the code in the previous section. Now you can deploy the contract. ## Deploying (originating) to the testnet Deploying a contract to the network is called "originating." Originating the contract requires a small amount of Tezos tokens as a fee. 1. On the left side of the page, under **Actions**, click **Deploy**. You may see a warning that the initial storage is not set. You can ignore this warning because you can set the initial storage now. 2. In the Deploy contract window, in the **Init storage** field, set the initial value for the contract's storage to an integer. 3. In the **Signer** field, make sure your account is selected. 4. Click **Estimate**. The window shows the estimated fees to deploy the contract, as in this picture: The estimate of the fees to deploy the contract 5. Click **Deploy**. The deployment process can take a few minutes. When the contract is deployed, the Deploy contract window shows the address at the bottom of the window. 6. Copy the address of the deployed contract, which starts with `KT1`. :::warning Copy the contract address now, because it will not be shown again. ::: Now you can call the contract from any Tezos client, including web applications and command-line applications like [The Octez client](/developing/octez-client). ## Calling the contract These steps show you how to inspect the contract with a block explorer, which is a web application that shows information about Tezos. It also allows you to call the contract. 1. Open the block explorer Better Call Dev at this link: https://better-call.dev/ 2. Paste the address of the contract in the search field and press Enter. The block explorer shows information about the contract, including recent transactions and the current state of its storage. The block explorer, showing information about the contract 3. Try calling one of the entrypoints: 1. Go to the **Storage** tab and check the current state of the storage, which should be the integer that you put in the Deploy window. 2. Go to the **Interact** tab. This tab shows the entrypoints in the contract and lets you use them. 3. For the `increment` entrypoint, in the **Parameters** section, put an integer in the field, as shown in this image: Putting in a value for an entrypoint parameter 4. Click **Execute** and then click **Wallet**. 5. Select your wallet and connect it to the application. 6. Confirm the transaction in your wallet. 7. Wait for a success message that says "The transaction has successfully been broadcasted to the network." 8. Go back to the **Storage** tab and see the new value of the storage, as in this picture: Updated storage value ## Summary Now the contract is running on the Tezos blockchain. You or any other user can call it from any source that can send transactions to Tezos, including Octez, dApps, and other contracts. If you want to continue working with this contract, here are some ideas: * Change permissions for the contract so only your account can call its entrypoints * Add your own entrypoints and originate a new contract; note that you cannot update the existing contract after it is deployed * Create a dApp to call the contract from a web application, similar to the dApp that you create in the tutorial [Build a simple web application](/tutorials/build-your-first-app/) # Deploy a smart contract with SmartPy Estimated time: 30 minutes This tutorial covers writing and deploying a simple smart contract with the SmartPy programming language. SmartPy has syntax similar to Python, but you don't need any experience with Python or SmartPy to do this tutorial. * If you are more familiar with OCaml, try [Deploy a smart contract with CameLIGO](/tutorials/smart-contract/cameligo). * If you are more familiar with JavaScript, try [Deploy a smart contract with JsLIGO](/tutorials/smart-contract/jsligo). * To learn the Archetype language, try [Deploy a smart contract with Archetype](/tutorials/smart-contract/archetype). SmartPy is a high-level programming language that you can use to write smart contracts for the Tezos blockchain. It abstracts away the complexity of using Michelson (the smart contract language directly available on-chain) to make it easier to write smart contracts on Tezos. In this tutorial, you will learn how to: * Create a wallet to manage an account containing cryptocurrency tokens * Get free tez tokens (the native cryptocurrency token on Tezos) from a faucet * Code a contract in SmartPy, including: * Creating a contract in the online IDE * Defining the storage for the contract * Defining entrypoints in the contract * Deploy (or originate) the contract to Tezos and set its starting storage value * Look up the current state of the contract * Call the contract ## What is a smart contract? A smart contract is a computer program that is stored on a blockchain and runs on a blockchain. Because the blockchain is spread across many computer nodes, you don't have to think about where to host the program or worry whether a computer will run it or not. Responsibility for running the contract is distributed across all of the nodes in the Tezos system, so when you deploy a smart contract, you can be confident that it will be available and unmodified when someone wants to run it. A smart contract has these parts: * Persistent storage, data that the contract can read and write * 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 programming languages * A Tezos account that can store tokens (technically, the contract is itself a type of Tezos account, but you can think of it as a program with a Tezos account) ## Tutorial contract The contract that you deploy in this tutorial stores a string value. It provides entrypoints that clients can call to change the value of that string: * The `replace` entrypoint accepts a new string as a parameter and stores that string, replacing the existing string. * The `append` entrypoint accepts a new string as a parameter and appends it to the existing string. After you deploy the contract, you or any other user can call it from various sources, including web applications, other contracts, and the Octez command-line client. However, no one can prevent it from running or tamper with its code or its storage. ## Creating and funding a wallet To deploy and work with the contract, you need a wallet and some tez tokens. 1. Install a Tezos-compatible wallet. Which wallet you install is up to you and whether you want to install a wallet on your computer, in a browser extension, or as a mobile app. For options, see [Installing and funding a wallet](/developing/wallet-setup) 2. Switch the wallet to use the Shadownet testnet instead of Tezos Mainnet. Shadownet is a network for testing Tezos applications where tokens are free so you don't have to spend real currency to work with your applications. For example, for the Temple browser wallet: 1. Expand the menu at top right and then turn on **Testnet mode**, as in this picture: Setting testnet mode in Temple 2. Above the list of tokens, click the display options button: Clicking the button to open display options 3. Under **Filter by network**, expand **All Networks**. 4. Select **Shadownet**: Selecting Shadownet in the network settings Now Temple shows your token balances on the Shadownet test network. 3. From your wallet, get the address of your account, which starts with `tz1`. This is the address that applications use to work with your wallet. 4. Go to the Shadownet faucet page at https://faucet.shadownet.teztnets.com. 5. On the faucet page, connect your wallet, or paste your wallet address into the input field labeled "Fund any address" and click the button for the amount of tez to add to your wallet. 20 tez is enough to work with the tutorial contract, and you can return to the faucet later if you need more tez. Depending on the amount you requested, it may take a few seconds or minutes for the faucet to send the tokens and for those tokens to appear in your wallet. You can use the faucet as much as you need to get tokens on the testnet, but those tokens are worthless and cannot be used on Mainnet. ![Funding your wallet using the Shadownet Faucet](/img/tutorials/wallet-funding.png) Now you have an account and funds that you can use to work with Tezos. ## Creating the contract The contract that you will create has these basic parts: * A function that initializes the contract and sets the starting value for its storage. * Functions called entrypoints that run code when clients call the contract. * Automated tests that verify that the contract works as expected. Follow these steps to create the code for the contract: 1. Open the SmartPy online IDE at https://smartpy.io/ide. You can work with SmartPy code in any IDE, but this online IDE keeps you from having to install software on your computer, and it also simplifies the process of deploying contracts. 2. In the code editor, add this line of code to import SmartPy: ```python import smartpy as sp ``` 3. Add this code that creates the entrypoints: ```python @sp.module def main(): class StoreGreeting(sp.Contract): def __init__(self, greeting): # Note the indentation # Initialize the storage with a string passed at deployment time # Cast the greeting parameter to a string sp.cast(greeting, sp.string) self.data.greeting = greeting @sp.entrypoint # Note the indentation def replace(self, params): self.data.greeting = params.text @sp.entrypoint # Note the indentation def append(self, params): self.data.greeting += params.text ``` Indentation is significant in Python, so make sure that your indentation matches this code. The first two lines create a SmartPy module, which indicates that the code is SmartPy instead of ordinary Python. Then the code creates a class named StoreGreeting, which represents the smart contract. The contract has an `__init__` function, which runs when the contract is deployed. In this case, the function sets the initial value of the storage to a parameter that you pass when you deploy the contract. This storage value is a string, but the storage can be another primitive type such as an integer or timestamp, or a complex data type that contains multiple values. For more information on contract data types, see [Data types](/smart-contracts/data-types). 4. Add this code, which creates automated tests: ```python # Automated tests that run on simulation @sp.add_test() def test(): # Initialize the test scenario scenario = sp.test_scenario("StoreGreeting", main) scenario.h1("StoreGreeting") # Initialize the contract and pass the starting value contract = main.StoreGreeting("Hello") scenario += contract # Verify that the value in storage was set correctly scenario.verify(contract.data.greeting == "Hello") # Test the entrypoints and check the new storage value contract.replace(text = "Hi") contract.append(text = ", there!") scenario.verify(contract.data.greeting == "Hi, there!") ``` When you run the SmartPy file, SmartPy runs a simulation in which it tests and compiles the contract. In this case, the tests verify that the replace and append endpoints work. For more information about SmartPy and tests, see the [SmartPy documentation](https://smartpy.tezos.com/). The SmartPy online IDE looks like this: ![The SmartPy online IDE, including the code for the contract](/img/tutorials/smartpy-ide-contract.png) The complete contract looks like this: ```python import smartpy as sp @sp.module def main(): class StoreGreeting(sp.Contract): def __init__(self, greeting): # Note the indentation # Initialize the storage with a string passed at deployment time # Cast the greeting parameter to a string sp.cast(greeting, sp.string) self.data.greeting = greeting @sp.entrypoint # Note the indentation def replace(self, params): self.data.greeting = params.text @sp.entrypoint # Note the indentation def append(self, params): self.data.greeting += params.text # Automated tests that run on simulation @sp.add_test() def test(): # Initialize the test scenario scenario = sp.test_scenario("Test scenario", main) scenario.h1("StoreGreeting") # Initialize the contract and pass the starting value contract = main.StoreGreeting("Hello") scenario += contract # Verify that the value in storage was set correctly scenario.verify(contract.data.greeting == "Hello") # Test the entrypoints and check the new storage value contract.replace(text = "Hi") contract.append(text = ", there!") scenario.verify(contract.data.greeting == "Hi, there!") ``` ## Testing and compiling the contract Before you can deploy the contract to Tezos, you must run it in the IDE, which automatically compiles it to Michelson, the base language of Tezos contracts. Then, the IDE also automatically runs the tests. 1. Compile the contract and run the tests by clicking the **Run Code** button: ![](/img/tutorials/smartpy-ide-run.png) The right-hand pane of the online IDE shows the results of the simulation, compilation, and testing process. The first step is simulating the deployment (origination) of the contract. The simulation assigns the contract a temporary address and shows the initial state of its storage: The originated contract and the initial storage in the SmartPy IDE Then, the simulation runs the test cases and shows the results of each call to an entrypoint: The results of the entrypoint calls ## Deploying (originating) to the testnet Deploying a contract to the network is called "originating." Originating the contract requires a small amount of Tezos tokens as a fee. 1. Under the origination step, click **Deploy contract**. The originated contract, with the Deploy contract button highlighted The IDE shows the compiled Michelson code of the contract, which is the language that smart contracts use on Tezos. 2. Below the Michelson code, click **Continue**. 3. In the new window, under "Node and Network," select the Shadownet testnet and accept the default RPC node, as in this picture: Selecting the Shadownet network and default RPC node 4. Under "Wallet," click **Select Account**. 5. In the pop-up window, connect your wallet. For most wallets, use the octez.connect (or Beacon) tab. 6. When your wallet is connected, click **Validate**. The Origination page shows your wallet information: The successful connection to your wallet on the origination page 7. Under "Contract Origination Parameters," click **Estimate cost**. The Fee field shows the estimated cost to deploy the contract in tez. 8. At the bottom of the page, click **Deploy Contract**. 9. In the pop-up window, click **Accept**. 10. Approve the transaction in your wallet app. The "Result" section shows information about the deployed contract, including its address: Information about the originated contract 11. Copy the contract address, which starts with `KT1`. :::note Be sure to save the contract address because it is not shown in the SmartPy online IDE again. If you close the window and forget the address, you can look up your account address in a block explorer; the block explorer shows your recent transactions, including smart contracts that you deployed. ::: 12. Open the contract in the block explorer Better Call Dev: 1. Click **Open explorer**. The IDE shows information about the contract and links to popular block explorers. 2. Click **Explore with Better Call Dev**. You can also go directly to https://better-call.dev/ in a new browser tab and search for the contract by its address. The block explorer shows information about the contract, including recent transactions and the current state of its storage: The block explorer, showing information about the contract 13. Try calling one of the entrypoints: 1. Go to the **Storage** tab and check the current state of the storage. If you just originated the contract, the storage is "Hello" because that's the value set in the smart contract code. 2. Go to the **Interact** tab. This tab shows the entrypoints in the contract and lets you use them. 3. For the `append` entrypoint, in the **Parameters** section, put some text in the field, as shown in this image: Putting in a value for an entrypoint parameter 4. Click **Execute** and then click **Wallet**. 5. Select your wallet and connect it to the application. 6. Confirm the transaction in your wallet. 7. Wait for a success message that says "The transaction has successfully been broadcasted to the network." 8. Go back to the **Storage** tab and see that the text that you put in the parameter has been added to the contract storage, as in this picture: Updated storage value ## Summary Now the contract is running on the Tezos blockchain. You or any other user can call it from any source that can send transactions to Tezos, including Octez, dApps, and other contracts. If you want to continue working with this contract, here are some ideas: * Change permissions for the contract so only your account can call its entrypoints * Add your own entrypoints and originate a new contract; note that you cannot update the existing contract after it is deployed * Create a dApp to call the contract from a web application, similar to the dApp that you create in the tutorial [Build a simple web application](/tutorials/build-your-first-app/) # Deploy a smart contract with Archetype Estimated time: 30 minutes This tutorial covers writing a smart contract and deploying it to Tezos in the Archetype programming language. It uses the completium-cli command-line tool, which lets you work with Archetype contracts and Tezos from the command line. * If you are more familiar with Python, try [Deploy a smart contract with SmartPy](/tutorials/smart-contract/smartpy). * If you are more familiar with OCaml, try [Deploy a smart contract with CameLIGO](/tutorials/smart-contract/cameligo). * If you are more familiar with JavaScript, try [Deploy a smart contract with JsLIGO](/tutorials/smart-contract/jsligo). In this tutorial, you will learn how to: * Create a wallet to store cryptocurrency tokens * Get free tez tokens (the native cryptocurrency token on Tezos) from a faucet * Code a contract in Archetype, including: * Defining the storage for the contract and its initial value * Defining entrypoints in the contract * Writing code to run when the entrypoints are called * Deploy (or originate) the contract to Tezos * Look up the current state of the contract * Call the contract from the command line ## What is a smart contract? A smart contract is a computer program that is stored on a blockchain and runs on a blockchain. Because the blockchain is spread across many computer nodes, you don't have to think about where to host the program or worry whether a computer will run it or not. Responsibility for running the contract is distributed across all of the nodes in the Tezos system, so when you deploy a smart contract, you can be confident that it will be available and unmodified when someone wants to run it. A smart contract has these parts: * Persistent storage, data that the contract can read and write * 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 programming languages * A Tezos account that can store tokens (technically, the contract is itself a type of Tezos account, but you can think of it as a program with a Tezos account) ## The Archetype language Archetype is a high-level language designed specifically for writing Tezos smart contracts. It has features that help you write smart contracts, including: * Clear syntax that maps closely with how smart contracts work * Enhancements that simplify working with storage * Tools that help you verify conditions before running code, such as ensuring that the caller is authorized to run the entrypoint * The ability to set up a contract as a state machine, which gives the contract a state and manages transitions between states * The ability to verify that the contract does what it says it does through the process of formal verification Like the other languages that Tezos accepts, Archetype code is compiled to Michelson to run on the blockchain. For more information about Archetype, see https://archetype-lang.org/. ## Tutorial contract The contract that you deploy in this tutorial stores a single integer. It provides entrypoints that clients can call to change the value of that integer: * The `increment` entrypoint accepts an integer as a parameter and adds that integer to the value in storage * The `decrement` entrypoint accepts an integer as a parameter and subtracts that integer from the value in storage * The `reset` entrypoint takes no parameters and resets the value in storage to 0 After you deploy the contract, you or any other user can call it from various sources, including web applications, other contracts, and the Octez command-line client. However, no one can prevent it from running or tamper with its code or its storage. ## Prerequisites To run this tutorial, you need the completium-cli program: 1. Make sure that NPM is installed by running this command in your command-line terminal: ```bash npm --version ``` If NPM is not installed, install Node.JS on your computer, which includes NPM, from this link: https://nodejs.org/en. 2. Install completium-cli by running this command: ```bash npm install -g @completium/completium-cli ``` You can verify that completium-cli installed by running this command: ```bash completium-cli version ``` If you see a message with the version of completium-cli, it is installed correctly. 3. Initialize completium-cli by running this command: ```bash completium-cli init ``` ## Using a testnet Before you deploy your contract to the main Tezos network (referred to as *Mainnet*), you can deploy it to a testnet. Testnets are useful for testing Tezos operations because testnets provide tokens for free so you can work with them without spending real tokens. Tezos testnets are listed on this site: https://teztnets.com/. The [Shadownet](https://teztnets.com/shadownet-about) testnet is a good choice for testing because it is intended to be long-lived, as opposed to shorter-term testnets that allow people to test new Tezos features. By default, completium-cli uses Shadownet, but these steps verify the network: 1. Verify that completium-cli is set to use Shadownet by running this command: ```bash completium-cli show endpoint ``` The response shows the RPC endpoint that completium-cli is using, which is its access point to the Tezos network. If the response shows `Current network: shadow`, it is using Shadownet. 2. If completium-cli is not using Shadownet, switch to Shadownet by running this command, selecting any endpoint labeled "shadownet," and pressing Enter: ```bash completium-cli switch endpoint ``` ## Creating a local wallet Deploying and using a smart contract costs fees, so you need a local wallet and XTZ tokens. You could use the default accounts that are included in completium-cli, but follow these steps to create your own local wallet on a test network: 1. Run the following command to generate a local wallet, replacing `local_wallet` with a name for your wallet: ```bash completium-cli generate account as local_wallet ``` 2. Switch to the account that you created by running this command, selecting the new account, and pressing Enter: ```bash completium-cli switch account ``` 3. Get the address for the wallet by running this command: ```bash completium-cli show account ``` The result shows the address of the account, which begins with "tz1". You need the wallet address to send funds to the wallet, to deploy the contract, and to send transactions to the contract. 4. Copy the address for the account, which is labeled as the "public key hash" in the response to the previous command. The address starts with "tz1". 5. On the testnets page at https://teztnets.com/, click the faucet link for the Shadownet testnet, which is at https://faucet.shadownet.teztnets.com. 6. On the faucet page, paste your wallet address into the input field labeled "Or fund any address" and click the button for the amount of XTZ to add to your wallet. 1 XTZ is enough for the tutorial. It may take a few minutes for the faucet to send the tokens and for those tokens to appear in your wallet. You can use the faucet as much as you need to get tokens on the testnet, but those tokens are worthless and cannot be used on Mainnet. ![Fund your wallet using the Shadownet Faucet](/img/tutorials/wallet-funding.png) 7. Run this command to check the balance of your wallet: ```bash completium-cli show account ``` If your wallet is set up correctly and the faucet has sent tokens to it, the response includes the balance of your wallet. ## Create the contract The contract that you will create has these basic parts: * A variable that represents the contract's storage, in this case an integer. Contracts can have storage in the form of primitive types such as an integer, string, or timestamp, or a complex data type that contains multiple values. For more information on contract data types, see [Data types](/smart-contracts/data-types). * Internal functions called entrypoints that run code when clients call the contract. Follow these steps to create the code for the contract: 1. Run this command to create the contract file: ```bash touch counter.arl ``` 2. Open the `counter.arl` file in any text editor. 3. At the top of the file, name the contract by putting the name after the `archetype` keyword: ```archetype archetype Counter ``` 4. Define the storage for the contract by adding this line: ```archetype variable value : int = 10 ``` This line creates a variable in the contract's storage with the name "value." It is an integer type and has the initial value of 10. Any variables that you create with the `variable` keyword at the top level of the contract become part of its persistent storage. 5. Add the code for the increment and decrement entrypoints: ```archetype // Increment entrypoint entry increment(delta : int) { value += delta } // Decrement entrypoint entry decrement(delta : int) { value -= delta } ``` These functions begin with the `entry` keyword to indicate that they are entrypoints. They accept one parameter: the change in the storage value, which is an integer named `delta`. One function adds the parameter to the value of the `value` variable and the other subtracts it. 6. Add this code for the reset entrypoint: ```archetype // Reset entrypoint entry reset() { value := 0 } ``` This function is similar to the others, but it does not take a parameter. It always sets the `value` variable to 0. The complete contract code looks like this: ```archetype archetype Counter variable value : int = 0 // Increment entrypoint entry increment(delta : int) { value += delta } // Decrement entrypoint entry decrement(delta : int) { value -= delta } // Reset entrypoint entry reset() { value := 0 } ``` ## Deploying (originating) to the testnet Deploying a contract to the network is called "originating." Originating the contract requires a small amount of Tezos tokens as a fee. 1. Run the following command to originate the smart contract: ```bash completium-cli deploy Counter.arl ``` The command line shows information about the transaction, including the name of the originating account, the target network, and the cost to deploy it. By default, it uses the local alias "Counter" to refer to the contract. 2. Press Y to confirm and deploy the contract. If you see an error that includes the message `contract.counter_in_the_past`, you waited too long before pressing Y. Run the `deploy` command again and promptly press Y to confirm it. 3. Print information about the contract by running this command: ```bash completium-cli show contract Counter ``` The response shows information about the contract, including its address on Shadownet, which starts with "KT1". You can use this information to look up the contract on a block explorer. 4. Verify that the contract deployed successfully by finding it on a block explorer: 1. Open a Tezos block explorer such as [TzKT](https://tzkt.io) or [Better Call Dev](https://better-call.dev/). 2. Set the explorer to Shadownet instead of Mainnet. 3. Paste the contract address, which starts with `KT1`, into the search field and press Enter. 4. Go to the Storage tab to see that the initial value of the storage is 10. 5. Run this command to see the current value of the contract storage: ```bash completium-cli show storage Counter ``` ## Calling the contract Now you can call the contract from any Tezos client, including completium-cli. To increment the current storage by a certain value, call the `increment` entrypoint, as in this example: ```bash completium-cli call Counter --entry increment --arg '{ "int": 5 }' ``` The CLI shows information about the call including the network and transaction fee and prompts you to press `Y` to confirm before it sends the transaction to the network. The "Operation injected" message indicates that the Completium CLI sent the transaction successfully. To decrement the storage, call the `decrement` entrypoint, as in this example: ```bash completium-cli call Counter --entry decrement --arg '{ "int": 2 }' ``` Finally, to reset the current storage to zero, call the `reset` entrypoint, as in this example: ```bash completium-cli call Counter --entry reset ``` Then, you can verify the updated storage on the block explorer or by running the `completium-cli show storage Counter` command. ## Summary Now the contract is running on the Tezos blockchain. You or any other user can call it from any source that can send transactions to Tezos, including command-line clients, dApps, and other contracts. If you want to continue working with this contract, here are some ideas: * Change permissions for the contract so only your account can call its entrypoints * Add your own entrypoints and originate a new contract; note that you cannot update the existing contract after it is deployed * Create a dApp to call the contract from a web application, similar to the dApp that you create in the tutorial [Build a simple web application](/tutorials/build-your-first-app/) # Create your minimum dapp on Tezos Estimated time: 2 hours > dApp : A decentralized application is a type of distributed open source software application that runs on a peer-to-peer (P2P) blockchain network rather than on a single computer. DApps are visibly similar to other software applications that are supported on a website or mobile device. This tutorial shows you how to create a poke game on smart contract. The game consists on poking the owner of a smart contract. The smart contract keeps a track of user interactions and stores a trace. Poke sequence diagram. ```mermaid sequenceDiagram Note left of User: Prepare poke transaction User->>Smartcontract: poke() Note right of Smartcontract: store(pokeTrace) ``` You will learn : * How to create a Tezos project with Taqueria. * How to create a smart contract in JsLIGO. * How to deploy the smart contract a real testnet named Ghostnet. * How to create a frontend dApp using Taquito library and interact with a Tezos browser wallet. * How to use an indexer like TZKT. ## Prerequisites This tutorial uses Typescript, so it will be easier if you are familiar with JavaScript. 1. Make sure that you have installed these tools: * [Node.JS and NPM](https://nodejs.org/en/download/): NPM is required to install the web application's dependencies. * [Taqueria](https://taqueria.io/), version 0.45.0 or later: Taqueria is a platform that makes it easier to develop and test dApps. * [Docker](https://docs.docker.com/engine/install/): Docker is required to run Taqueria. * [jq](https://stedolan.github.io/jq/download/): Some commands use the `jq` program to extract JSON data. * [`yarn`](https://yarnpkg.com/): The frontend application uses yarn to build and run (see this article for details about [differences between `npm` and `yarn`](https://www.geeksforgeeks.org/difference-between-npm-and-yarn/)). * Any Tezos-compatible wallet that supports Ghostnet, such as [Temple wallet](https://templewallet.com/). 2. Optionally, you can install [`VS Code`](https://code.visualstudio.com/download) to edit your application code in and the [LIGO VS Code extension](https://marketplace.visualstudio.com/items?itemName=ligolang-publish.ligo-vscode) for LIGO editing features such as code highlighting and completion. Taqueria also provides a [Taqueria VS Code extension](https://marketplace.visualstudio.com/items?itemName=ecadlabs.taqueria-vscode) that helps visualize your project and run tasks. ## The tutorial application In this tutorial, you create a simple game where the user is poking though a dApp. The user interacts with the smart contract through a web interface, where they can see the current state of the contract and send poke commands to it. The contract responds by updating its storage with the user's address. Alternately, a user can also poke the contract deployed by other users. The application looks like this: ![Example of the table of addresses and which addresses poked them](/img/tutorials/dapp-table.png) The code for the completed application is in this GitHub repository: [solution](https://github.com/marigold-dev/training-dapp-1/tree/main/solution) When you're ready, move to the next section [Create your minimum dApp on Tezos](/tutorials/dapp/part-1) to begin setting up the application. # Part 1: Create your minimum dApp on Tezos To start working with the application, you create a Taqueria project and use it to deploy the Poke contract. Then you set up a web application to connect with a wallet, and then interact with your smart contract. Before you begin, make sure that you have installed the tools in the [Prerequisites](/tutorials/dapp#prerequisites) section. ## Creating a Taqueria project Taqueria manages the project structure and keeps it up to date. For example, when you deploy a new smart contract, Taqueria automatically updates the web app to send transactions to that new smart contract. Follow these steps to set up a Taqueria project: On the command-line terminal, run these commands to set up a Taqueria project and install the LIGO and Taquito plugins: ```bash taq init training cd training taq install @taqueria/plugin-ligo taq install @taqueria/plugin-taquito taq create contract pokeGame.jsligo ``` ## Write the smart contract 1. Edit the **pokeGame.jsligo** file. Remove the default code and paste this code instead. ```jsligo export type storage = unit; type return_ = [list, storage]; @entry const poke = (_: unit, store: storage): return_ => { return [list([]), store]; }; ``` Every contract has to follow these rules : * At least one entrypoint, annotated with **@entry** , with a mandatory signature taking 2 arguments ***(parameter, storage)** and a return type. An entrypoint is a function that is exposed as an external API. * **parameter**: the entrypoint parameter. Mandatory and can be of any type. For example: an (ignored) variable starting with`_` here, and of type `unit` (the type void on LIGO). * **storage**: the on-chain storage. Mandatory and can be of any type. For example, here we use the type `unit`. It is recommended to add an `export` keyword before the type definition as it is a good practice to export it when you are required to write unit tests from another LIGO file. * **return_**: a mandatory pair of list of `operation` and the storage type (defined earlier). Return type naming is free but don't use an existing keyword like **return**. [Have a look at the Entrypoints contracts documentation](/smart-contracts/entrypoints)> > Note: Previous versions of LIGO used a single main function instead of a function for each entrypoint. This syntax is still valid, but it is harder to read and deprecated in LIGO V1. > > A `Poke` variant parameter is generated from the `poke` entrypoint function under the hood. A variant is more or less equivalent of the Enum type in Javascript. A default main function is generated and act like as a dispatcher for each of your entrypoints. It means that this painful boilerplate is no more needed on the new syntax. [Have a look at the Variant type documentation](/smart-contracts/data-types/complex-data-types#variants) 2. Write the poke function. The objective is to store every user/caller addresses poking the contract. Rewrite the storage, and add the caller address to the set of traces. At line 1, replace the line with: ```jsligo export type storage = set
; ``` 3. Replace the `poke` function with: ```jsligo @entry const poke = (_: unit, store: storage): return_ => { return [list([]), Set.add(Tezos.get_source(), store)] }; ``` Explanation: * The LIGO Set library has a function **add** to add one element to the Set of items. There is no concept of Class in LIGO, you use a library to apply functions on objects. * A list of operations is required to return. An empty list is returned here as there is no other contract to call. [Have a look at the Set library documentation](https://ligo.tezos.com/docs/next/data-types/sets) [Have a look at the List library documentation](https://ligo.tezos.com/docs/next/data-types/lists) Here, get the caller address using `Tezos.get_source()`. Tezos library provides useful functions for manipulating blockchain objects. [Have a look at the Tezos library documentation](https://ligo.tezos.com/docs/reference/tezos.next-reference/) ## Simulate a call on your smart contract The LIGO command-line provides sub-commands to test your LIGO code. [Have a look at the Testing Framework documentation](https://ligo.tezos.com/docs/next/testing/) 1. Compile the contract with Taqueria (Force to use a specific LIGO version with `TAQ_LIGO_IMAGE` Taqueria environment variable). ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq compile pokeGame.jsligo ``` Taqueria is generating the `.tz` Michelson file in the `artifacts` folder. The Michelson language is the default stack language used by the Michelson VM to run your code on a node. It is something similar to WASM. [Have a look on the Michelson documentation](https://octez.tezos.com/docs/active/michelson.html) 2. Taqueria is generating two additional files, edit the first file `pokeGame.storageList.jsligo` replacing the current code with: ```jsligo #import "pokeGame.jsligo" "Contract" const default_storage = Set.empty as set
; ``` When you deploy a contract, you are required to initialize the default state of your smart contract. Taqueria allows you to declare different variables on this file, it is useful to use different initialized states per environment. [Have a look at the Taqueria documentation](https://taqueria.io/docs/plugins/plugin-ligo/#the-taq-compile-task) 3. Compile all (contract + initial storage) ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq compile pokeGame.jsligo ``` It compiles both source code and storage. Before deployment, to simulate a call to our entrypoint **poke**, Taq has a **taq simulate** command. The contract parameter `Poke()` and the initial storage with the default empty set are passed to the execution. 4. Edit the second file **pokeGame.parameterList.jsligo** ```jsligo #import "pokeGame.jsligo" "Contract" const default_parameter: parameter_of Contract = Poke(); ``` 5. Run the simulation. First, install the Tezos client plugin, recompile it all, and then run the simulation. ```bash taq install @taqueria/plugin-octez-client TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq compile pokeGame.jsligo taq simulate pokeGame.tz --param pokeGame.parameter.default_parameter.tz ``` Output logs: ```logs β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Contract β”‚ Result β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ pokeGame.tz β”‚ storage β”‚ β”‚ β”‚ { "tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU" } β”‚ β”‚ β”‚ emitted operations β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ big_map diff β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` You can notice that the instruction is storing the address of the caller in the storage set. ### Configure your wallet and deploy The default Tezos testing testnet is called **Ghostnet**. > :warning: You need an account to deploy a contract with some `tez` (the Tezos native currency). The first time you deploy a contract with Taqueria, it is generating a new user account with `0 tez`. 1. Deploy your contract to the `testing` environment. Ut forces Taqueria to generate a default account on a testing config file. ```bash taq deploy pokeGame.tz -e "testing" ``` You should get this kind of log. ```log Warning: the faucet field in network configs has been deprecated and will be ignored. A keypair with public key hash tz1XXXXXXXXXXXXXXXXXXXXXX was generated for you. To fund this account: 1. Go to https://teztnets.com and click "Faucet" of the target testnet. 2. Copy and paste the above key into the 'wallet address field. 3. Request some Tez (Note that you might need to wait for a few seconds for the network to register the funds). No operations performed. ``` * Choice NΒ°1 (Recommended): Use Alice wallet instead of the generated account. A common usage is to use **alice** account as Taqueria operator. **alice** is a commonly known address used on Tezos and she has always some **tez**. Replace the Taqueria config file for **testing** env **.taq/config.local.testing.json** with **alice** settings: ```json { "networkName": "ghostnet", "accounts": { "taqOperatorAccount": { "publicKey": "edpkvGfYw3LyB1UcCahKQk4rF2tvbMUk8GFiTuMjL75uGXrpvKXhjn", "publicKeyHash": "tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb", "privateKey": "edsk3QoqBuvdamxouPhin7swCvkQNgq4jP5KZPbwWNnwdZpSpJiEbq" } } } ``` * Choice NΒ°2: use the Taqueria-generated account. Copy the account **privateKey** from the **.taq/config.local.testing.json** config file. Open your Temple browser extension on your computer or on your mobile phone and do the [initial setup](https://www.youtube.com/watch?v=S8_tL8PfCts). Once you are done, go to Settings (click on the avatar icon, or display Temple in full page) and click on **Import account > Private key** tab. Paste the **privateKey** to Temple text input and confirm. Send free Tez to your new account via this web faucet [here](https://teztnets.com/). Connect your wallet on **Ghostnet** and ask for free tez. Now you have some money to play with. 2. Deploy to Ghostnet testnet. ```bash taq deploy pokeGame.tz -e "testing" ``` Your smart contract is deployed on the Ghostnet. ```logs β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Contract β”‚ Address β”‚ Alias β”‚ Balance In Mutez β”‚ Destination β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ pokeGame.tz β”‚ KT1G8tx4qSeJmKRY1p2oxA6eYoCGc9Qi3Fky β”‚ pokeGame β”‚ 0 β”‚ https://ghostnet.ecadinfra.com β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ## Create the frontend ### Create a React app ```bash yarn create vite ``` Then follow the prompts. Choose React and then Typescript+SWC: ```shell ? Project name: β€Ί app #Enter your project name ? Select a framework: β€Ί - Use arrow-keys. Return to submit. # Select React as framework Vanilla Vue ❯ React Preact Lit Svelte Others ? Select a variant: β€Ί - Use arrow-keys. Return to submit. #Both TypeScript variants are fine. Select TypeScript only. TypeScript ❯ TypeScript + SWC JavaScript JavaScript + SWC ``` [More information about SWC here](https://swc.rs/). 1. Add taquito and tzkt indexer libraries. ```bash cd app yarn add @taquito/taquito @taquito/beacon-wallet @tezos-x/octez.connect-sdk @tzkt/sdk-api yarn add -D @tezos-x/octez.connect-types ``` > :warning: Before starting, add the following dependencies in order to resolve polyfill issues. Some dependencies are from NodeJs, thus not included in browsers. 2. For example, in my case, I installed this: ```bash yarn add --dev process buffer crypto-browserify stream-browserify assert stream-http https-browserify os-browserify url path-browserify ``` 3. Create a new file `nodeSpecific.ts` in the src folder of your project and edit with this content: ```bash touch src/nodeSpecific.ts ``` ```js import { Buffer } from 'buffer'; globalThis.Buffer = Buffer; ``` 4. Open the `index.html` file and replace the `body` with this one: ```html
``` 5. Open the `vite.config.ts` file and replace it with: ```js import react from '@vitejs/plugin-react-swc'; import path from 'path'; import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default ({ command }) => { const isBuild = command === 'build'; return defineConfig({ define: {}, plugins: [react()], build: { commonjsOptions: { transformMixedEsModules: true, }, }, resolve: { alias: { // dedupe @tezos-x/octez.connect-sdk // I almost have no idea why it needs `cjs` on dev and `esm` on build, but this is how it works πŸ€·β€β™‚οΈ '@tezos-x/octez.connect-sdk': path.resolve( path.resolve(), `./node_modules/@tezos-x/octez.connect-sdk/dist/${ isBuild ? 'esm' : 'cjs' }/index.js` ), stream: 'stream-browserify', os: 'os-browserify/browser', util: 'util', process: 'process/browser', buffer: 'buffer', crypto: 'crypto-browserify', assert: 'assert', http: 'stream-http', https: 'https-browserify', url: 'url', path: 'path-browserify', }, }, }); }; ``` ### Generate the Typescript classes from Michelson code and run the server Taqueria is able to generate Typescript classes for any frontend application. It takes the definition of your smart contract and generates the contract entrypoint functions, type definitions, etc ... To get typescript classes from Taqueria plugin, on your project root folder run: ```bash taq install @taqueria/plugin-contract-types taq generate types ./app/src ``` 1. Go back to your frontend app and run the dev server. ```bash cd app yarn dev ``` 2. Open your browser at: http://localhost:5173/ Your app should be running. ### Connect / disconnect the wallet Declare two React Button components and display the user's address and balance. 1. Edit **src/App.tsx** file: ```typescript import { NetworkType } from '@tezos-x/octez.connect-types'; import { BeaconWallet } from '@taquito/beacon-wallet'; import { TezosToolkit } from '@taquito/taquito'; import * as api from '@tzkt/sdk-api'; import { useEffect, useState } from 'react'; import './App.css'; import ConnectButton from './ConnectWallet'; import DisconnectButton from './DisconnectWallet'; function App() { api.defaults.baseUrl = 'https://api.ghostnet.tzkt.io'; const [Tezos, setTezos] = useState( new TezosToolkit('https://ghostnet.ecadinfra.com') ); const [wallet, setWallet] = useState( new BeaconWallet({ name: 'Training', preferredNetwork: NetworkType.GHOSTNET, }) ); useEffect(() => { (async () => { const activeAccount = await wallet.client.getActiveAccount(); if (activeAccount) { setUserAddress(activeAccount.address); const balance = await Tezos.tz.getBalance(activeAccount.address); setUserBalance(balance.toNumber()); } })(); }, []); const [userAddress, setUserAddress] = useState(''); const [userBalance, setUserBalance] = useState(0); return (
I am {userAddress} with {userBalance} mutez
); } export default App; ``` 2. Let's create the 2 missing src component files: ```bash touch src/ConnectWallet.tsx touch src/DisconnectWallet.tsx ``` ConnectWallet button creates an instance wallet, gets user permissions via a popup, and then retrieves the current account information. 3. Edit **ConnectWallet.tsx** ```typescript import { NetworkType } from '@tezos-x/octez.connect-sdk'; import { BeaconWallet } from '@taquito/beacon-wallet'; import { TezosToolkit } from '@taquito/taquito'; import { Dispatch, SetStateAction } from 'react'; type ButtonProps = { Tezos: TezosToolkit; setUserAddress: Dispatch>; setUserBalance: Dispatch>; wallet: BeaconWallet; setTezos: Dispatch>; }; const ConnectButton = ({ Tezos, setTezos, setUserAddress, setUserBalance, wallet, }: ButtonProps): JSX.Element => { const connectWallet = async (): Promise => { try { await wallet.requestPermissions({ network: { type: NetworkType.GHOSTNET, rpcUrl: 'https://ghostnet.ecadinfra.com', }, }); // gets user's address const userAddress = await wallet.getPKH(); const balance = await Tezos.tz.getBalance(userAddress); setUserBalance(balance.toNumber()); setUserAddress(userAddress); Tezos.setWalletProvider(wallet); setTezos(Tezos); } catch (error) { console.log(error); } }; return (
); }; export default ConnectButton; ``` 4. Edit **DisconnectWallet.tsx** The button cleans the wallet instance and all linked objects. ```typescript import { BeaconWallet } from '@taquito/beacon-wallet'; import { Dispatch, SetStateAction } from 'react'; interface ButtonProps { wallet: BeaconWallet; setUserAddress: Dispatch>; setUserBalance: Dispatch>; } const DisconnectButton = ({ wallet, setUserAddress, setUserBalance, }: ButtonProps): JSX.Element => { const disconnectWallet = async (): Promise => { setUserAddress(''); setUserBalance(0); console.log('disconnecting wallet'); await wallet.clearActiveAccount(); }; return (
); }; export default DisconnectButton; ``` 5. Save both files, the dev server should refresh the page. As Temple is configured, click on Connect button. On the popup, select your Temple wallet, then your account, and connect. ![The app after you have connected, showing your address and tex balance](/img/tutorials/dapp-logged.png) Your are *logged*. 6. Click on the Disconnect button to test the disconnection, and then reconnect. ### List other poke contracts via an indexer Instead of querying heavily the RPC node to search where are located all other similar contracts and retrieve each address, use an indexer. an indexer is a kind of enriched cache API on top of an RPC node. In this example, the TZKT indexer is used to find other similar contracts. 1. You need to install jq to parse the Taqueria JSON configuration file. [Install jq](https://github.com/stedolan/jq) 2. On `package.json`, change the `dev` command on `scripts` configuration. Prefix it with a `jq` command to create an new environment variable pointing to your last smart contract address on testing env: ```bash "dev": "jq -r '\"VITE_CONTRACT_ADDRESS=\" + last(.tasks[]).output[0].address' ../.taq/testing-state.json > .env && vite", ``` The last deployed contract address on Ghostnet is set now on our frontend. 3. Add a button to fetch all similar contracts like yours, then display the list. Edit **App.tsx** and before the `return` of App function, add this section for the fetch function. ```typescript const [contracts, setContracts] = useState>([]); const fetchContracts = () => { (async () => { setContracts( await api.contractsGetSimilar(import.meta.env.VITE_CONTRACT_ADDRESS, { includeStorage: true, sort: { desc: 'id' }, }) ); })(); }; ``` 4. On the returned **html template** section, after the display of the user balance div `I am {userAddress} with {userBalance} mutez`, append this: ```tsx
{contracts.map((contract) =>
{contract.address}
)}
``` 5. Save your file and restart your server. Now, the start script generates the .env file containing the last deployed contract address. ```bash yarn dev ``` 6. Go to your web browser and click on **Fetch contracts** button. ![](/img/tutorials/dapp-deployedcontracts.png) Congratulations, you are able to list all similar deployed contracts. ### Poke your contract 1. Import the Taqueria-generated types on **app/src/App.tsx**. ```typescript import { PokeGameWalletType } from './pokeGame.types'; ``` 2. Add this new function after the previous fetch function, it calls the entrypoint for poking. ```typescript const poke = async (contract: api.Contract) => { let c: PokeGameWalletType = await Tezos.wallet.at( '' + contract.address ); try { const op = await c.methodsObject.default().send(); await op.confirmation(); alert('Tx done'); } catch (error: any) { console.table(`Error: ${JSON.stringify(error, null, 2)}`); } }; ``` > :warning: Normally, a call to `c.methods.poke()` function is expected by convention, but with an unique entrypoint, Michelson generates a unique `default` entrypoint name instead of having the name of the entrypoint function. Also, be careful because all entrypoints function names are in lowercase, and all parameter types are in uppercase. 3. Replace the line displaying the contract address `{contracts.map((contract) =>
{contract.address}
)}` with the one below, it adds a Poke button. ```html {contracts.map((contract) =>
{contract.address}
)} ``` 4. Save and see the page refreshed, then click on the Poke button. ![](/img/tutorials/dapp-pokecontracts.png) It calls the contract and adds your public address tz1... to the set of traces. 5. Display poke guys To verify that on the page, we can display the list of poke people directly on the page Replace again the html previous line `{contracts ...}` with this one ```html {contracts.map((contract) => )}
addresspeopleaction
{contract.address}{contract.storage.join(", ")}
``` Contracts are displaying their people now ![](/img/tutorials/dapp-table.png) > :information_source: Wait around few second for blockchain confirmation and click on `fetch contracts` to refresh the list :confetti_ball: Congratulations, you have completed this first dapp training ## Summary Now, you can create any smart contract using LIGO and create a complete Dapp via Taqueria/Taquito. In the next section, you will learn how to call a Smart contract from a smart contract using callbacks and also write unit and mutation tests. When you are ready, continue to [Part 2: Inter-contract calls and testing](/tutorials/dapp/part-2). # Part 2: Inter-contract calls and testing Previously, you learned how to create your first dApp. In this second session, you will enhance your skills on: * How to do inter-contract calls. * How to use views. * How to do unit & mutation tests. On the first version of the Poke game, you were able to poke any deployed contract. Now, you will add a new function to store on the trace an additional feedback message coming from another contract. ## Poke and Get Feedback sequence diagram ```mermaid sequenceDiagram Note left of User: Prepare to poke Smartcontract2 though Smartcontract1 User->>Smartcontract1: pokeAndGetFeedback(Smartcontract2) Smartcontract1->>Smartcontract2 : getFeedback() Smartcontract2->>Smartcontract1 : pokeAndGetFeedbackCallback([Tezos.get_self_address(),store.feedback]) Note left of Smartcontract1: store Smartcontract2 address + feedback from Smartcontract2 ``` ## Get the code Get the code from the first session: https://github.com/marigold-dev/training-dapp-1/blob/main/solution ```bash git clone https://github.com/marigold-dev/training-dapp-1.git ``` Reuse the code from the previous smart contract: https://github.com/marigold-dev/training-dapp-1/blob/main/solution/contracts/pokeGame.jsligo Install all libraries locally: ```bash cd solution && npm i && cd app && yarn install && cd .. ``` ## Modify the poke function Change the storage to reflect the changes: * If you poke directly, you must register the contract's owner's address and no feedback. * If you poke and ask to get feedback from another contract, then you register the other contract address and an additional feedback message. Here is the new sequence diagram of the poke function. ```mermaid sequenceDiagram Note left of User: Prepare to poke Smartcontract1 User->>Smartcontract1: poke() Note left of Smartcontract1: store User address + no feedback ``` 1. Edit `./contracts/pokeGame.jsligo` and replace the storage definition with this one: ```jsligo export type pokeMessage = { receiver : address, feedback : string }; export type storage = { pokeTraces : map, feedback : string }; ``` 2. Replace your poke function with these lines: ```jsligo @entry const poke = (_ : unit, store : storage) : return_ => { let feedbackMessage = {receiver : Tezos.get_self_address() ,feedback: ""}; return [ list([]) as list, {...store, pokeTraces : Map.add(Tezos.get_source(), feedbackMessage, store.pokeTraces) }]; }; ``` Explanation: * `...store` do a copy by value of your object. Note: you cannot do an assignment like this `store.pokeTraces=...` in jsLIGO. * `Map.add(...`: Add a key, value entry to a map. For more information about [Map](https://ligo.tezos.com/docs/next/data-types/maps). * `export type storage = {...};` a `Record` type is declared, it is an object structure known in LIGO as a [Record](https://ligo.tezos.com/docs/next/data-types/records). * `Tezos.get_self_address()` is a native function that returns the current contract address running this code. Have a look at [Tezos native functions](https://ligo.tezos.com/docs/next/reference/toplevel-reference). * `feedback: ""`: poking directly does not store feedback. 3. Edit `pokeGame.storageList.jsligo` to change the storage initialization. ```jsligo #import "pokeGame.jsligo" "Contract" const default_storage: Contract.storage = { pokeTraces: Map.empty as map, feedback: "kiss" }; ``` 4. Compile your contract. ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq compile pokeGame.jsligo ``` Write a second function `pokeAndGetFeedback` involving the call to another contract a bit later, let's do unit testing first! ## Write unit tests 1. Add a new unit test smart-contract file `unit_pokeGame.jsligo`. ```bash taq create contract unit_pokeGame.jsligo ``` > :information_source: Testing documentation can be found [here](https://ligo.tezos.com/docs/next/testing/) 2. Edit the file. ```jsligo #import "./pokeGame.jsligo" "PokeGame" export type main_fn = module_contract; // reset state const _ = Test.reset_state(2 as nat, list([]) as list); const faucet = Test.nth_bootstrap_account(0); const sender1: address = Test.nth_bootstrap_account(1); const _2 = Test.log("Sender 1 has balance : "); const _3 = Test.log(Test.get_balance_of_address(sender1)); const _4 = Test.set_baker(faucet); const _5 = Test.set_source(faucet); export const initial_storage = { pokeTraces: Map.empty as map, feedback: "kiss" }; export const initial_tez = 0mutez; //functions export const _testPoke = ( taddr: typed_address, s: address ): unit => { const contr = Test.to_contract(taddr); const contrAddress = Tezos.address(contr); Test.log("contract deployed with values : "); Test.log(contr); Test.set_source(s); const status = Test.transfer_to_contract(contr, Poke(), 0 as tez); Test.log(status); const store: PokeGame.storage = Test.get_storage(taddr); Test.log(store); //check poke is registered match(Map.find_opt(s, store.pokeTraces)) { when (Some(pokeMessage)): do { assert_with_error( pokeMessage.feedback == "", "feedback " + pokeMessage.feedback + " is not equal to expected " + "(empty)" ); assert_with_error( pokeMessage.receiver == contrAddress, "receiver is not equal" ); } when (None()): assert_with_error(false, "don't find traces") }; }; // TESTS // const testSender1Poke = ( (): unit => { const orig = Test.originate(contract_of(PokeGame), initial_storage, initial_tez); _testPoke(orig.addr, sender1); } )(); ``` Explanations: * `#import "./pokeGame.jsligo" "PokeGame"` to import the source file as a module to call functions and use object definitions. * `export type main_fn` it will be useful later for the mutation tests to point to the main function to call/mutate. * `Test.reset_state ( 2...` this creates two implicit accounts on the test environment. * `Test.nth_bootstrap_account` This returns the nth account from the environment. * `Test.to_contract(taddr)` and `Tezos.address(contr)` are util functions to convert typed addresses, contract, and contract addresses. * `let _testPoke = (s : address) : unit => {...}` declaring function starting with `_` is escaping the test for execution. Use this to factorize tests changing only the parameters of the function for different scenarios. * `Test.set_source` do not forget to set this value for the transaction signer. * `Test.transfer_to_contract(CONTRACT, PARAMS, TEZ_COST)` A transaction to send, it returns an operation. * `Test.get_storage` This is how to retrieve the contract's storage. * `assert_with_error(CONDITION,MESSAGE)` Use assertion for unit testing. * `const testSender1Poke = ...` This test function will be part of the execution report. * `Test.originate_module(MODULE_CONVERTED_TO_CONTRACT,INIT_STORAGE, INIT_BALANCE)` It originates a smart contract into the Test environment. A module is converted to a smart contract. 3. Run the test ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq test unit_pokeGame.jsligo ``` The output should give you intermediary logs and finally the test results. ```logs β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Contract β”‚ Test Results β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ unit_pokeGame.jsligo β”‚ "Sender 1 has balance : " β”‚ β”‚ β”‚ 3800000000000mutez β”‚ β”‚ β”‚ "contract deployed with values : " β”‚ β”‚ β”‚ KT1KwMWUjU6jYyLCTWpZAtT634Vai7paUnRN(None) β”‚ β”‚ β”‚ Success (2130n) β”‚ β”‚ β”‚ {feedback = "kiss" ; pokeTraces = [tz1TDZG4vFoA2xutZMYauUnS4HVucnAGQSpZ -> {feedback = "" ; receiver = KT1KwMWUjU6jYyLCTWpZAtT634Vai7paUnRN}]} β”‚ β”‚ β”‚ Everything at the top-level was executed. β”‚ β”‚ β”‚ - testSender1Poke exited with value (). β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ πŸŽ‰ All tests passed πŸŽ‰ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ## Do an inter-contract call To keep things simple, 2 versions of the same smart contract are deployed to simulate inter-contract calls and get the feedback message (cf. [sequence diagram](#poke-and-get-feedback-sequence-diagram)). Create a new poke function `PokeAndGetFeedback: (other : address)` with a second part function `PokeAndGetFeedbackCallback: (feedback : returned_feedback)` as a callback. Calling a contract is asynchronous, this is the reason it is done two times. The function to call on the second contract is `GetFeedback: (contract_callback: oracle_param)` and returns a feedback message. > Very often, this kind of contract is named an `Oracle`, because generally its storage is updated by an offchain scheduler and it exposes data to any onchain smart contracts. 1. Edit the file `pokeGame.jsligo`, to define new types: ```jsligo type returned_feedback = [address, string]; //address that gives feedback and a string message type oracle_param = contract; ``` Explanations : * `type returned_feedback = [address, string]` the parameters of an oracle function always start with the address of the contract caller and followed by the return objects. * `type oracle_param = contract` the oracle parameters need to be wrapped inside a typed contract. 2. Write the missing functions, starting with `getFeedback`. Add this new function at the end of the file. ```jsligo @entry const getFeedback = (contract_callback : contract, store : storage): return_ => { let op : operation = Tezos.transaction( [Tezos.get_self_address(),store.feedback], (0 as mutez), contract_callback); return [list([op]) ,store]; }; ``` * `Tezos.transaction(RETURNED_PARAMS,TEZ_COST,CALLBACK_CONTRACT)` the oracle function requires to return the value back to the contract caller that is passed already as first parameter. * `return [list([op]) ,store]` this time, you return a list of operations to execute, there is no need to update the contract storage (but it is a mandatory return object). 3. Add now, the first part of the function `pokeAndGetFeedback`. ```jsligo @entry const pokeAndGetFeedback = (oracleAddress: address, store: storage): return_ => { //Prepares call to oracle let call_to_oracle = (): contract => { return match( Tezos.get_entrypoint_opt("%getFeedback", oracleAddress) as option> ) { when (None()): failwith("NO_ORACLE_FOUND") when (Some(contract)): contract }; }; // Builds transaction let op: operation = Tezos.transaction( ( ( Tezos.self("%pokeAndGetFeedbackCallback") as contract ) ), (0 as mutez), call_to_oracle() ); return [list([op]), store]; }; ``` * `Tezos.get_entrypoint_opt("%getFeedback",oracleAddress)` you require to get the oracle contract address. Then you want to call a specific entrypoint of this contract. The function name is always starting with `%` with always the first letter in lowercase (even if the code is different). * `Tezos.transaction(((Tezos.self("%pokeAndGetFeedbackCallback") as contract)),TEZ_COST,call_to_oracle())` The transaction takes as first param the entrypoint of for the callback that the oracle uses to answer the feedback, the tez cost and the oracle contract you got just above as transaction destination. 4. Write the last missing function `pokeAndGetFeedbackCallback`, receive the feedback and finally store it. ```jsligo @entry const pokeAndGetFeedbackCallback = (feedback : returned_feedback, store : storage) : return_ => { let feedbackMessage = {receiver : feedback[0] ,feedback: feedback[1]}; return [ list([]) as list, {...store, pokeTraces : Map.add(Tezos.get_source(), feedbackMessage , store.pokeTraces) }]; }; ``` * `let feedbackMessage = {receiver : feedback[0] ,feedback: feedback[1]}` prepares the trace including the feedback message and the feedback contract creator. * `{...store,pokeTraces : Map.add(Tezos.get_source(), feedbackMessage , store.pokeTraces) }` add the new trace to the global trace map. 5. Compile the contract. ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq compile pokeGame.jsligo ``` 6. (Optional) Write a unit test for this new function `pokeAndGetFeedback`. ## Use views instead of inter-contract call As you saw in the previous step, inter-contract calls make the business logic more complex but not only that, thinking about the cost is even worse, as described in [Optimisation](https://ligo.tezos.com/docs/next/tutorials/optimisation/) in the LIGO documentation. In this training, the oracle is providing a read-only storage that can be replaced by a `view` instead of a complex and costly callback. [See the documentation here about onchain views](https://ligo.tezos.com/docs/next/syntax/contracts/views). ```mermaid sequenceDiagram Note left of User: Prepare to poke on Smartcontract1 and get feedback from Smartcontract2 User->>Smartcontract1: pokeAndGetFeedback(Smartcontract2) Smartcontract1-->>Smartcontract2 : feedback() Smartcontract2-->>Smartcontract1 : [Smartcontract2,feedback] Note left of Smartcontract1: store Smartcontract2 address + feedback from Smartcontract2 ``` :warning: **Comment below functions (with `/* */` syntax or // syntax) or just remove it, it is no more useful** :warning: * `pokeAndGetFeedbackCallback` * `getFeedback` 1. Edit function `pokeAndGetFeedback` to call view instead of a transaction. ```jsligo @entry const pokeAndGetFeedback = (oracleAddress: address, store: storage): return_ => { //Read the feedback view let feedbackOpt: option = Tezos.call_view("feedback", unit, oracleAddress); match(feedbackOpt) { when (Some(feedback)): do { let feedbackMessage = { receiver: oracleAddress, feedback: feedback }; return [ list([]) as list, { ...store, pokeTraces: Map.add( Tezos.get_source(), feedbackMessage, store.pokeTraces ) } ]; } when (None()): failwith("Cannot find view feedback on given oracle address") }; }; ``` 2. Declare the view at the end of the file. Do not forget the annotation `@view` ! ```jsligo @view export const feedback = (_: unit, store: storage): string => { return store.feedback }; ``` 3. Compile the contract. ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq compile pokeGame.jsligo ``` 4. (Optional) Write a unit test for the updated function `pokeAndGetFeedback`. ## Write mutation tests LIGO provides mutation testing through the Test library. Mutation tests are like `testing your tests` to see if your unit test coverage is strong enough. Bugs, or mutants, are automatically inserted into your code. Your tests are run on each mutant. If your tests fail then the mutant is killed. If your tests passed, the mutant survived. The higher the percentage of mutants killed, the more effective your tests are. [Example of mutation features for other languages](https://stryker-mutator.io/docs/mutation-testing-elements/supported-mutators) 1. Create a file `mutation_pokeGame.jsligo`. ```bash taq create contract mutation_pokeGame.jsligo ``` 2. Edit the file. ```jsligo #import "./pokeGame.jsligo" "PokeGame" #import "./unit_pokeGame.jsligo" "PokeGameTest" // reset state const _ = Test.reset_state(2 as nat, list([]) as list); const faucet = Test.nth_bootstrap_account(0); const sender1: address = Test.nth_bootstrap_account(1); const _1 = Test.log("Sender 1 has balance : "); const _2 = Test.log(Test.get_balance_of_address(sender1)); const _3 = Test.set_baker(faucet); const _4 = Test.set_source(faucet); const _tests = ( ta: typed_address, _: michelson_contract, _2: int ): unit => { return PokeGameTest._testPoke(ta, sender1); }; const test_mutation = ( (): unit => { const mutationErrorList = Test.originate_and_mutate_all( contract_of(PokeGame), PokeGameTest.initial_storage, PokeGameTest.initial_tez, _tests ); match(mutationErrorList) { when ([]): unit when ([head, ..._tail]): do { Test.log(head); Test.assert_with_error(false, Test.to_string(head[1])) } }; } )(); ``` Explanation: * `#import `: import your source code that will be mutated and your unit tests. For more information, see [Namespaces](https://ligo.tezos.com/docs/next/syntax/modules?lang=jsligo) in the LIGO documentation. * `const _tests = (ta: typed_address, _: michelson_contract, _: int) : unit => {...`: you need to provide the test suite that will be run by the framework. Just point to the unit test you want to run. * `const test_mutation = (() : unit => {`: this is the definition of the mutations tests. * `Test.originate_module_and_mutate_all(CONTRACT_TO_MUTATE, INIT_STORAGE, INIT_TEZ_COST, UNIT_TEST_TO_RUN)`: This will take the first argument as the source code to mutate and the last argument as unit test suite function to run over. It returns a list of mutations that succeed (if size > 0 then bad test coverage) or an empty list (good, even mutants did not harm your code). 3. Run the test. ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq test mutation_pokeGame.jsligo ``` Output: ```logs === Error messages for mutation_pokeGame.jsligo === File "contracts/mutation_pokeGame.jsligo", line 43, characters 12-66: 42 | Test.log(head); 43 | Test.assert_with_error(false, Test.to_string(head[1])) 44 | } Test failed with "Mutation at: File "contracts/pokeGame.jsligo", line 52, characters 15-66: 51 | when (None()): 52 | failwith("Cannot find view feedback on given oracle address") 53 | }; Replacing by: "Cannot find view feedback on given oracle addressCannot find view feedback on given oracle address". " Trace: File "contracts/mutation_pokeGame.jsligo", line 43, characters 12-66 , File "contracts/mutation_pokeGame.jsligo", line 43, characters 12-66 , File "contracts/mutation_pokeGame.jsligo", line 28, character 2 to line 47, character 5 === β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Contract β”‚ Test Results β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ mutation_pokeGame.jsligo β”‚ Some tests failed :( β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` Invaders are here. What happened? The mutation has altered a part of the code that is not tested, it was not covered, so the unit test passed. For a short fix, tell the Library to ignore this function for mutants. 4. Go to your source file pokeGame.jsligo, and annotate the function `pokeAndGetFeedback` with `@no_mutation`. ```jsligo @no_mutation @entry const pokeAndGetFeedback ... ``` 5. Run again the mutation tests. ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq test mutation_pokeGame.jsligo ``` Output ```logs β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Contract β”‚ Test Results β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ mutation_pokeGame.jsligo β”‚ "Sender 1 has balance : " β”‚ β”‚ β”‚ 3800000000000mutez β”‚ β”‚ β”‚ "contract deployed with values : " β”‚ β”‚ β”‚ KT1L8mCbuTJXKq3CDoHDxqfH5aj5sEgAdx9C(None) β”‚ β”‚ β”‚ Success (1330n) β”‚ β”‚ β”‚ {feedback = "kiss" ; pokeTraces = [tz1hkMbkLPkvhxyqsQoBoLPqb1mruSzZx3zy -> {feedback = "" ; receiver = KT1L8mCbuTJXKq3CDoHDxqfH5aj5sEgAdx9C}]} β”‚ β”‚ β”‚ "Sender 1 has balance : " β”‚ β”‚ β”‚ 3800000000000mutez β”‚ β”‚ β”‚ Everything at the top-level was executed. β”‚ β”‚ β”‚ - test_mutation exited with value (). β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ πŸŽ‰ All tests passed πŸŽ‰ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ## Update the frontend 1. Reuse the dApp files from [the previous session](https://github.com/marigold-dev/training-dapp-1/tree/main/solution/app). 2. Redeploy a new version of the smart contract. > Note: You can set `feedback` value to any action other than default `kiss` string (it is more fun for other to discover it). ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq compile pokeGame.jsligo taq generate types ./app/src taq deploy pokeGame.tz -e "testing" ``` 3. Adapt the frontend application code. Edit `App.tsx`, and add new import. ```typescript import { address } from './type-aliases'; ``` 4. Add a new React variable after `userBalance` definition. ```typescript const [contractToPoke, setContractToPoke] = useState(''); ``` 5. Change the poke function to set entrypoint to `pokeAndGetFeedback`. ```typescript //poke const poke = async ( e: React.FormEvent, contract: api.Contract ) => { e.preventDefault(); let c: PokeGameWalletType = await Tezos.wallet.at('' + contract.address); try { const op = await c.methodsObject .pokeAndGetFeedback(contractToPoke as address) .send(); await op.confirmation(); alert('Tx done'); } catch (error: any) { console.log(error); console.table(`Error: ${JSON.stringify(error, null, 2)}`); } }; ``` 6. Change the display to a table changing `contracts.map...` by: ```html {contracts.map((contract) => )}
addresstrace "contract - feedback - user"action
{contract.address}{(contract.storage !== null && contract.storage.pokeTraces !== null && Object.entries(contract.storage.pokeTraces).length > 0)?Object.keys(contract.storage.pokeTraces).map((k : string)=>contract.storage.pokeTraces[k].receiver+" "+contract.storage.pokeTraces[k].feedback+" "+k+", "):""}
poke(e,contract)}>setContractToPoke(e.currentTarget.value)} placeholder='enter contract address here' />
``` 7. Relaunch the app. ```bash cd app yarn install yarn dev ``` On the listed contract, choose your line and input the address of the contract you will receive feedback. Click on `poke`. ![The dApp page showing the result of the poke action](/img/tutorials/dapp-result.png). This time, the logged user will receive feedback from a targeted contract (as input of the form) via any listed contract (the first column of the table). Refresh manually by clicking on `Fetch` contracts\` button. Poke other developers' contracts to discover their contract hidden feedback when you poke them. ## Summary Now, you can call other contracts, use views, and test your smart contract before deploying it. In the next training, you will learn how to use tickets. When you are ready, continue to [Part 3: Tickets](/tutorials/dapp/part-3). # Part 3: Tickets Previously, you learned how to do inter-contract calls, use views, and do unit testing. In this third session, you will enhance your skills on: * Using tickets. * Don't mess up with `DUP` errors while manipulating tickets. On the second version of the poke game, you were able to poke any contract without constraint. A right to poke via tickets is now mandatory. Tickets are a kind of object that cannot be copied and can hold some trustable information. ## new Poke sequence diagram ```mermaid sequenceDiagram Admin->>Smartcontract : Init(User,1) Note right of Smartcontract : Mint 1 ticket for User Note left of User : Prepare to poke User->>Smartcontract : Poke Note right of Smartcontract : Check available tickets for User Note right of Smartcontract : Store trace and burn 1 ticket Smartcontract-->>User : success User->>Smartcontract : Poke Note right of Smartcontract : Check available tickets for User Smartcontract-->>User : error ``` ## Prerequisites Prerequisites are the same as the first session: https://github.com/marigold-dev/training-dapp-1#memo-prerequisites. Get the code from the session 2 solution [here](https://github.com/marigold-dev/training-dapp-2/tree/main/solution). ## Tickets Tickets came with a Tezos **Edo** upgrade, they are great and often misunderstood. Ticket structure: * Ticketer: (address) the creator contract address. * Value: (any) Can be any type from string to bytes. It holds whatever arbitrary values. * Amount: (nat) quantity of tickets minted. Tickets features: * Not comparable: it makes no sense to compare tickets because tickets of the same type are all equal and can be merged into a single ticket. When ticket types are different then it is no more comparable. * Transferable: you can send a ticket into a Transaction parameter. * Storable: only on smart contract storage for the moment (Note: a new protocol release will enable it for use accounts soon). * Non-dupable: you cannot copy or duplicate a ticket, it is a unique singleton object living in a specific blockchain instance. * Splittable: if the amount is > 2 then you can split the ticket object into 2 objects. * Mergeable: you can merge tickets from the same ticketer and the same type. * Mintable/burnable: anyone can create and destroy tickets. Example of usage: * Authentication and Authorization token: giving a ticket to a user provides you with Authentication. Adding some claims/rules on the ticket provides you with some rights. * Simplified FA1.2/FA2 token: representing crypto token with tickets (mint/burn/split/join), but it does not have all the same properties and does not respect the TZIP standard. * Voting rights: giving 1 ticket that counts for 1 vote on each member. * Wrapped crypto: holding XTZ collateral against a ticket, and redeeming it later. * Many others ... ## Minting Minting is the action of creating a ticket from the void. In general, minting operations are done by administrators of smart contracts or either by an end user. 1. Edit the `./contracts/pokeGame.jsligo` file and add a map of ticket ownership to the default `storage` type. This map keeps a list of consumable tickets for each authorized user. It is used as a burnable right to poke. ```jsligo export type storage = { pokeTraces: map, feedback: string, ticketOwnership: map> //ticket of claims }; ``` To fill this map, add a new administration endpoint. A new entrypoint `Init` is adding x tickets to a specific user. > Note: to simplify, there is no security around this entrypoint, but in Production it should. Tickets are very special objects that cannot be **DUPLICATED**. During compilation to Michelson, using a variable twice, copying a structure holding tickets generates `DUP` command. To avoid our contract failing at runtime, LIGO parses statically our code during compilation time to detect any DUP on tickets. To solve most of the issues, segregate ticket objects from the rest of the storage, or structures containing ticket objects to avoid compilation errors. To do this, just destructure any object until you get tickets isolated. For each function having a storage as parameter, `store` object needs to be destructured to isolate `ticketOwnership` object holding our tickets. Then, don't use anymore the `store` object or it creates a **DUP** error. 2. Add the new `Init` function. ```jsligo @entry const init = ([a, ticketCount]: [address, nat], store: storage): return_ => { const { pokeTraces, feedback, ticketOwnership } = store; if (ticketCount == (0 as nat)) { return [ list([]) as list, { pokeTraces, feedback, ticketOwnership } ] } else { const t: ticket = Option.unopt(Tezos.create_ticket("can_poke", ticketCount)); return [ list([]) as list, { pokeTraces, feedback, ticketOwnership: Map.add(a, t, ticketOwnership) } ] } }; ``` The Init function looks at how many tickets to create from the current caller, and then it is added to the current map. 3. Modify the poke function. ```jsligo @entry const poke = (_: unit, store: storage): return_ => { const { pokeTraces, feedback, ticketOwnership } = store; const [t, tom]: [option>, map>] = Map.get_and_update( Tezos.get_source(), None() as option>, ticketOwnership ); return match(t) { when (None): failwith("User does not have tickets => not allowed") when (Some(_t)): [ list([]) as list, { feedback, pokeTraces: Map.add( Tezos.get_source(), { receiver: Tezos.get_self_address(), feedback: "" }, pokeTraces ), ticketOwnership: tom } ] } }; ``` First, extract an existing optional ticket from the map. If an operation is done directly on the map, even trying to find or get this object in the structure, a DUP Michelson instruction is generated. Use the secure `get_and_update` function from the Map library to extract the item from the map and avoid any copy. > Note: more information about this function, see [Updating maps](https://ligo.tezos.com/docs/next/data-types/maps?lang=jsligo#updating) in the LIGO documentation. In a second step, look at the optional ticket, if it exists, then burn it (destroy it) and add a trace of execution, otherwise fail with an error message. 4. Same for `pokeAndGetFeedback` function, do the same checks and type modifications as below. ```jsligo @no_mutation @entry const pokeAndGetFeedback = (oracleAddress: address, store: storage): return_ => { const { pokeTraces, feedback, ticketOwnership } = store; ignore(feedback); const [t, tom]: [option>, map>] = Map.get_and_update( Tezos.get_source(), None() as option>, ticketOwnership ); let feedbackOpt: option = Tezos.call_view("feedback", unit, oracleAddress); return match(t) { when (None): failwith("User does not have tickets => not allowed") when (Some(_t)): match(feedbackOpt) { when (Some(feedback)): do { let feedbackMessage = { receiver: oracleAddress, feedback: feedback }; return [ list([]) as list, { feedback, pokeTraces: Map.add( Tezos.get_source(), feedbackMessage, pokeTraces ), ticketOwnership: tom } ] } when (None): failwith("Cannot find view feedback on given oracle address") } } }; ``` 5. Update the storage initialization on `pokeGame.storageList.jsligo`. ```jsligo #import "pokeGame.jsligo" "Contract" const default_storage = { pokeTraces: Map.empty as map, feedback: "kiss", ticketOwnership: Map.empty as map< address, ticket > //ticket of claims }; ``` 6. Compile the contract to check for any errors. > Note: don't forget to check that Docker is running for taqueria. ```bash npm i TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq compile pokeGame.jsligo ``` Check on logs that everything is fine. Try to display a DUP error now. 7. Add this line on `poke function` just after the first line of storage destructuration `const { pokeTraces, feedback, ticketOwnership } = store;`. ```jsligo const t2 = Map.find_opt(Tezos.get_source(), ticketOwnership); ``` 8. Compile again. ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq compile pokeGame.jsligo ``` This time you should see the `DUP` warning generated by the **find** function. ```logs Warning: variable "ticketOwnership" cannot be used more than once. ``` 9. Remove it. ## Test your code Update the unit test files to see if you can still poke. 1. Edit the `./contracts/unit_pokeGame.jsligo` file. ```jsligo #import "./pokeGame.jsligo" "PokeGame" export type main_fn = module_contract; const _ = Test.reset_state(2 as nat, list([]) as list); const faucet = Test.nth_bootstrap_account(0); const sender1: address = Test.nth_bootstrap_account(1); const _1 = Test.log("Sender 1 has balance : "); const _2 = Test.log(Test.get_balance_of_address(sender1)); const _3 = Test.set_baker(faucet); const _4 = Test.set_source(faucet); const initial_storage = { pokeTraces: Map.empty as map, feedback: "kiss", ticketOwnership: Map.empty as map> }; const initial_tez = 0 as tez; export const _testPoke = ( taddr: typed_address, s: address, ticketCount: nat, expectedResult: bool ): unit => { const contr = Test.to_contract(taddr); const contrAddress = Tezos.address(contr); Test.log("contract deployed with values : "); Test.log(contr); Test.set_source(s); const statusInit = Test.transfer_to_contract(contr, Init([sender1, ticketCount]), 0 as tez); Test.log(statusInit); Test.log("*** Check initial ticket is here ***"); Test.log(Test.get_storage(taddr)); const status: test_exec_result = Test.transfer_to_contract(contr, Poke(), 0 as tez); Test.log(status); const store: PokeGame.storage = Test.get_storage(taddr); Test.log(store); return match(status) { when (Fail(tee)): match(tee) { when (Other(msg)): assert_with_error(expectedResult == false, msg) when (Balance_too_low(_record)): assert_with_error(expectedResult == false, "ERROR Balance_too_low") when (Rejected(s)): assert_with_error(expectedResult == false, Test.to_string(s[0])) } when (Success(_n)): match( Map.find_opt( s, (Test.get_storage(taddr) as PokeGame.storage).pokeTraces ) ) { when (Some(pokeMessage)): do { assert_with_error( pokeMessage.feedback == "", "feedback " + pokeMessage.feedback + " is not equal to expected " + "(empty)" ); assert_with_error( pokeMessage.receiver == contrAddress, "receiver is not equal" ) } when (None()): assert_with_error(expectedResult == false, "don't find traces") } } }; const _5 = Test.log("*** Run test to pass ***"); const testSender1Poke = ( (): unit => { const orig = Test.originate(contract_of(PokeGame), initial_storage, initial_tez); _testPoke(orig.addr, sender1, 1 as nat, true) } )(); const _6 = Test.log("*** Run test to fail ***"); const testSender1PokeWithNoTicketsToFail = ( (): unit => { const orig = Test.originate(contract_of(PokeGame), initial_storage, initial_tez); _testPoke(orig.addr, sender1, 0 as nat, false) } )(); ``` * On `Init([sender1, ticketCount])`, initialize the smart contract with some tickets. * On `Fail`, check if you have an error on the test (i.e. the user should be allowed to poke). * On `testSender1Poke`, test with the first user using a preexisting ticket. * On `testSender1PokeWithNoTicketsToFail`, test with the same user again but with no ticket, and an error should be caught. 2. Run the test, and look at the logs to track execution. ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq test unit_pokeGame.jsligo ``` The first test should be fine. ```logs β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Contract β”‚ Test Results β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ unit_pokeGame.jsligo β”‚ "Sender 1 has balance : " β”‚ β”‚ β”‚ 3800000000000mutez β”‚ β”‚ β”‚ "*** Run test to pass ***" β”‚ β”‚ β”‚ "contract deployed with values : " β”‚ β”‚ β”‚ KT1HeEVF74BLi3fYCpr1tpkDGmruFBNjMATo(None) β”‚ β”‚ β”‚ Success (1858n) β”‚ β”‚ β”‚ "*** Check initial ticket is here ***" β”‚ β”‚ β”‚ {feedback = "kiss" ; pokeTraces = [] ; ticketOwnership = [tz1hkMbkLPkvhxyqsQoBoLPqb1mruSzZx3zy -> (KT1HeEVF74BLi3fYCpr1tpkDGmruFBNjMATo , ("can_poke" , 1n))]} β”‚ β”‚ β”‚ Success (1024n) β”‚ β”‚ β”‚ {feedback = "kiss" ; pokeTraces = [tz1hkMbkLPkvhxyqsQoBoLPqb1mruSzZx3zy -> {feedback = "" ; receiver = KT1HeEVF74BLi3fYCpr1tpkDGmruFBNjMATo}] ; ticketOwnership = []} β”‚ β”‚ β”‚ "*** Run test to fail ***" β”‚ β”‚ β”‚ "contract deployed with values : " β”‚ β”‚ β”‚ KT1HDbqhYiKs8e3LkNAcT9T2MQgvUdxPtbV5(None) β”‚ β”‚ β”‚ Success (1399n) β”‚ β”‚ β”‚ "*** Check initial ticket is here ***" β”‚ β”‚ β”‚ {feedback = "kiss" ; pokeTraces = [] ; ticketOwnership = []} β”‚ β”‚ β”‚ Fail (Rejected (("User does not have tickets => not allowed" , KT1HDbqhYiKs8e3LkNAcT9T2MQgvUdxPtbV5))) β”‚ β”‚ β”‚ {feedback = "kiss" ; pokeTraces = [] ; ticketOwnership = []} β”‚ β”‚ β”‚ Everything at the top-level was executed. β”‚ β”‚ β”‚ - testSender1Poke exited with value (). β”‚ β”‚ β”‚ - testSender1PokeWithNoTicketsToFail exited with value (). β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ πŸŽ‰ All tests passed πŸŽ‰ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` 3. Redeploy the smart contract. Let's play with the CLI to compile and deploy. ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq compile pokeGame.jsligo taq generate types ./app/src taq deploy pokeGame.tz -e testing ``` ```logs β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Contract β”‚ Address β”‚ Alias β”‚ Balance In Mutez β”‚ Destination β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ pokeGame.tz β”‚ KT1TC1DabCTmdMXuuCxwUmyb51bn2mbeNvbW β”‚ pokeGame β”‚ 0 β”‚ https://ghostnet.ecadinfra.com β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ## Adapt the frontend code 1. Rerun the app and check that you can not use the app anymore without tickets. ```bash cd app yarn dev ``` 2. Connect with any wallet with enough tez, and Poke your contract. ![pokefail](/img/tutorials/dapp-pokefail.png) The Kukai wallet is giving me back the error from the smart contract. ![kukaifail](/img/tutorials/dapp-kukaifail.png) Ok, so let's authorize some minting on my user and try again to poke. 3. Add a new button for minting on a specific contract, and replace the full content of `App.tsx` with: ```typescript import { NetworkType } from '@tezos-x/octez.connect-types'; import { BeaconWallet } from '@taquito/beacon-wallet'; import { TezosToolkit } from '@taquito/taquito'; import * as api from '@tzkt/sdk-api'; import { BigNumber } from 'bignumber.js'; import { useEffect, useState } from 'react'; import './App.css'; import ConnectButton from './ConnectWallet'; import DisconnectButton from './DisconnectWallet'; import { PokeGameWalletType, Storage } from './pokeGame.types'; import { address, nat } from './type-aliases'; function App() { api.defaults.baseUrl = 'https://api.ghostnet.tzkt.io'; const [Tezos, setTezos] = useState( new TezosToolkit('https://ghostnet.ecadinfra.com') ); const [wallet, setWallet] = useState( new BeaconWallet({ name: 'Training', preferredNetwork: NetworkType.GHOSTNET, }) ); const [contracts, setContracts] = useState>([]); const [contractStorages, setContractStorages] = useState< Map >(new Map()); const fetchContracts = () => { (async () => { const tzktcontracts: Array = await api.contractsGetSimilar( import.meta.env.VITE_CONTRACT_ADDRESS, { includeStorage: true, sort: { desc: 'id' }, } ); setContracts(tzktcontracts); const taquitoContracts: Array = await Promise.all( tzktcontracts.map( async (tzktcontract) => (await Tezos.wallet.at( tzktcontract.address! )) as PokeGameWalletType ) ); const map = new Map(); for (const c of taquitoContracts) { const s: Storage = await c.storage(); map.set(c.address, s); } setContractStorages(map); })(); }; useEffect(() => { (async () => { const activeAccount = await wallet.client.getActiveAccount(); if (activeAccount) { setUserAddress(activeAccount.address); const balance = await Tezos.tz.getBalance(activeAccount.address); setUserBalance(balance.toNumber()); } })(); }, []); const [userAddress, setUserAddress] = useState(''); const [userBalance, setUserBalance] = useState(0); const [contractToPoke, setContractToPoke] = useState(''); //poke const poke = async ( e: React.MouseEvent, contract: api.Contract ) => { e.preventDefault(); let c: PokeGameWalletType = await Tezos.wallet.at('' + contract.address); try { const op = await c.methodsObject .pokeAndGetFeedback(contractToPoke as address) .send(); await op.confirmation(); alert('Tx done'); } catch (error: any) { console.log(error); console.table(`Error: ${JSON.stringify(error, null, 2)}`); } }; //mint const mint = async ( e: React.MouseEvent, contract: api.Contract ) => { e.preventDefault(); let c: PokeGameWalletType = await Tezos.wallet.at('' + contract.address); try { console.log('contractToPoke', contractToPoke); const op = await c.methods .init(userAddress as address, new BigNumber(1) as nat) .send(); await op.confirmation(); alert('Tx done'); } catch (error: any) { console.log(error); console.table(`Error: ${JSON.stringify(error, null, 2)}`); } }; return (
I am {userAddress} with {userBalance} mutez

{contracts.map((contract) => ( ))}
address trace "contract - feedback - user" action
{contract.address} {contractStorages.get(contract.address!) !== undefined && contractStorages.get(contract.address!)!.pokeTraces ? Array.from( contractStorages .get(contract.address!)! .pokeTraces.entries() ).map( (e) => e[1].receiver + ' ' + e[1].feedback + ' ' + e[0] + ',' ) : ''} { console.log('e', e.currentTarget.value); setContractToPoke(e.currentTarget.value); }} placeholder="enter contract address here" />
); } export default App; ``` > Note: You maybe have noticed, but the full typed generated Taquito class is used for the storage access now. It improves maintenance in case you contract storage has changed. 4. Refresh the page, now that you have the Mint button. 5. Mint a ticket on this contract. ![mint](/img/tutorials/dapp-mint.png) 6. Wait for the Tx popup confirmation and then try to poke again, it should succeed now. ![success](/img/tutorials/dapp-success.png) 7. Wait for the Tx popup confirmation and try to poke again, you should be out of tickets and it should fail. ![kukaifail](/img/tutorials/dapp-kukaifail.png) Congratulations, you know how to use tickets and avoid DUP errors. > Takeaways: > > * You can go further and improve the code like consuming one 1 ticket quantity at a time and manage it the right way. > * You can also implement different type of Authorization mechanism, not only `can poke` claim. > * You can also try to base your ticket on some duration time like JSON token can do, not using the data field as a string but as bytes and store a timestamp on it. ## Summary Now, you understand tickets. For more information about tickets, see [Tickets](/smart-contracts/data-types/complex-data-types#tickets). In the next training, you will learn how to upgrade smart contracts. When you are ready, continue to [Part 4: Smart contract upgrades](/tutorials/dapp/part-4). # Part 4: Smart contract upgrades # Upgradable Poke game Previously, you learned how to use tickets and don't mess up with it. In this third session, you will enhance your skills on: * Upgrading a smart contract with lambda function code. * Upgrading a smart contract with proxy. As you may know, smart contracts are immutable but in real life, applications are not and evolve. During the past several years, bugs and vulnerabilities in smart contracts caused millions of dollars to get stolen or lost forever. Such cases may even require manual intervention in blockchain operation to recover the funds. Let's see some tricks that allow you to upgrade a contract. # Prerequisites There is nothing more than you need on the first session: https://github.com/marigold-dev/training-dapp-1#memo-prerequisites. Get the code from the session 3 or the solution [here](https://github.com/marigold-dev/training-dapp-3/tree/main/solution). # Upgrades As everyone knows, one feature of blockchain is to keep immutable code on a block. This allows transparency, traceability, and trustlessness. But the application lifecycle implies evolving and upgrading code to fix bugs or bring functionalities. So, how to do it? > https://gitlab.com/tezos/tzip/-/blob/master/proposals/tzip-18/tzip-18.md > Note: All below solutions break in a wait the fact that a smart contract is immutable. **Trust** preservation can be safe enough if the upgrade process has some security and authenticity around it. Like the first time an admin deploys a smart contract, any user should be able to trust the code reading it with free read access, the same should apply to the upgrade process (notification of new code version, admin identification, whitelisted auditor reports, ...). To resume, if you really want to avoid DEVOPS centralization, you are about to create a DAO with a voting process among some selected users/administrators in order to deploy the new version of the smart contract ... but let's simplify and talk here only about classical centralized admin deployment. ## Naive approach One can deploy a new version of the smart contract and do a redirection to the new address on the front end side. Complete flow. ```mermaid sequenceDiagram Admin->>Tezos: originate smart contract A Tezos-->>Admin: contractAddress A User->>frontend: click on %myfunction frontend->>SmartContractA: transaction %myfunction Note right of SmartContractA : executing logic of A Admin->>Tezos: originate smart contract B with A storage as init Tezos-->>Admin: contractAddress B Admin->>frontend: change smart contract address to B User->>frontend: click on %myfunction frontend->>SmartContractB: transaction %myfunction Note right of SmartContractB : executing logic of B ``` | Pros | Cons | | ------------- | ---------------------------------------------------------------------------------------------- | | Easiest to do | Old contract remains active, so do bugs. Need to really get rid off it | | | Need to migrate old storage, can cost a lot of money or even be too big to copy at init time | | | Need to sync/update frontend at each backend migration | | | Lose reference to previous contract address, can lead to issues with other dependent contracts | ## Stored Lambda function This time, the code will be on the storage and being executed at runtime. Init. ```mermaid sequenceDiagram Admin->>Tezos: originate smart contract with a lambda Map on storage, initialized Map.literal(list([["myfunction",""]])) Tezos-->>Admin: contractAddress ``` Interaction. ```mermaid sequenceDiagram User->>SmartContract: transaction %myfunction Note right of SmartContract : Tezos.exec(lambaMap.find_opt(myfunction)) ``` Administration. ```mermaid sequenceDiagram Admin->>SmartContract: transaction(["myfunction",""],0,updateLambdaCode) Note right of SmartContract : Check caller == admin Note right of SmartContract : Map.add("myfunction","",lambaMap) ``` ### Pros/Cons | Pros | Cons | | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | No more migration of code and storage. Update the lambda function code that is on existing storage | For the storage, all has to be stores as bytes PACKING/UNPACKING it so type checking is lost | | keep same contract address | IDE or tools do not work anymore on lambda code. Michelson does not protect us from some kinds of mistakes anymore | | | Unexpected changes can cause other contract callers to fail, Interface benefits is lost | | | Harder to audit and trace, can lead to really big security nd Trust issues | | | Storing everything as bytes is limited to PACK-able types like nat, string, list, set, map | ### Implementation Change the implementation of the function `pokeAndGetFeedback`. The feedback is now a lambda function on the storage. It is required to: * Add a new entrypoint to change the lambda code. * Update the current entrypoint for calling the lambda. 1. Let's start with adding the lambda function definition of the storage. ```jsligo export type feedbackFunction = (oracleAddress: address) => string; export type storage = { pokeTraces: map, feedback: string, ticketOwnership: map>, //ticket of claims feedbackFunction: feedbackFunction }; ``` Let's make minor changes as you have 1 additional field `feedbackFunction` on storage destructuring. 2. Edit the `PokeAndGetFeedback` function where the lambda `feedbackFunction(..)` is executed ```jsligo @no_mutation @entry const pokeAndGetFeedback = (oracleAddress: address, store: storage): return_ => { const { pokeTraces, feedback, ticketOwnership, feedbackFunction } = store; const [t, tom]: [option>, map>] = Map.get_and_update( Tezos.get_source(), None() as option>, ticketOwnership ); let feedbackMessage = { receiver: oracleAddress, feedback: feedbackFunction(oracleAddress) }; return match(t) { when (None()): failwith("User does not have tickets => not allowed") when (Some(_t)): [ list([]) as list, { feedback, pokeTraces: Map.add(Tezos.get_source(), feedbackMessage, pokeTraces), ticketOwnership: tom, feedbackFunction } ] } }; ``` Notice the line with `feedbackFunction(oracleAddress)` and call the lambda with the address parameter. The first time, the current code is injected to check that it still works, and then, modify the lambda code on the storage. 3. To modify the lambda function code, add an extra admin entrypoint `updateFeedbackFunction`. ```jsligo @entry const updateFeedbackFunction = (newCode: feedbackFunction, store: storage): return_ => { const { pokeTraces, feedback, ticketOwnership, feedbackFunction } = store; ignore(feedbackFunction); return [ list([]), { pokeTraces, feedback, ticketOwnership, feedbackFunction: newCode } ] }; ``` 4. The storage definition is broken, fix all storage missing field warnings on `poke` and `init` functions. ```jsligo @entry const poke = (_: unit, store: storage): return_ => { const { pokeTraces, feedback, ticketOwnership, feedbackFunction } = store; const [t, tom]: [option>, map>] = Map.get_and_update( Tezos.get_source(), None() as option>, ticketOwnership ); return match(t) { when (None()): failwith("User does not have tickets => not allowed") when (Some(_t)): [ list([]) as list, { feedback, pokeTraces: Map.add( Tezos.get_source(), { receiver: Tezos.get_self_address(), feedback: "" }, pokeTraces ), ticketOwnership: tom, feedbackFunction } ] } }; @entry const init = ([a, ticketCount]: [address, nat], store: storage): return_ => { const { pokeTraces, feedback, ticketOwnership, feedbackFunction } = store; if (ticketCount == (0 as nat)) { return [ list([]) as list, { pokeTraces, feedback, ticketOwnership, feedbackFunction } ] } else { const t: ticket = Option.unopt(Tezos.create_ticket("can_poke", ticketCount)); return [ list([]) as list, { pokeTraces, feedback, ticketOwnership: Map.add(a, t, ticketOwnership), feedbackFunction } ] } }; ``` 5. Change the initial storage with the old initial value of the lambda function (i.e. calling a view to get feedback). ```jsligo #import "pokeGame.jsligo" "Contract" const default_storage = { pokeTraces: Map.empty as map, feedback: "kiss", ticketOwnership: Map.empty as map>, //ticket of claims feedbackFunction: ( (oracleAddress: address): string => { return match( Tezos.call_view("feedback", unit, oracleAddress) as option ) { when (Some(feedback)): feedback when (None()): failwith( "Cannot find view feedback on given oracle address" ) }; } ) }; ``` 6. Compile and play with the CLI. ```bash npm i TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq compile pokeGame.jsligo ``` 7. Redeploy to testnet ```bash taq deploy pokeGame.tz -e testing ``` ```logs β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Contract β”‚ Address β”‚ Alias β”‚ Balance In Mutez β”‚ Destination β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ pokeGame.tz β”‚ KT1VjFawYQ4JeEEAVchqaYK1NmXCENm2ufer β”‚ pokeGame β”‚ 0 β”‚ https://ghostnet.ecadinfra.com β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` 8. Test the dApp frontend. Regenerate types and run the frontend. ```bash taq generate types ./app/src cd app yarn dev ``` 9. Run the user sequence on the web page: 1. Mint 1 ticket. 2. wait for confirmation. 3. poke a contract address. 4. wait for confirmation. 5. click on the button to refresh the contract list. So far so good, you have the same result as the previous training. Update the lambda function in the background with the CLI through the new admin entrypoint. Return a fixed string this time, just for demo purposes, and verify that the lambda executed is returning another output. 10. Edit the file `pokeGame.parameterList.jsligo`. ```jsligo #import "pokeGame.jsligo" "Contract" const default_parameter : parameter_of Contract = UpdateFeedbackFunction((_oracleAddress : address) : string => "YEAH!!!"); ``` 11. Compile all and call an init transaction. ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq compile pokeGame.jsligo taq call pokeGame --param pokeGame.parameter.default_parameter.tz -e testing ``` ```logs β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Contract Alias β”‚ Contract Address β”‚ Parameter β”‚ Entrypoint β”‚ Mutez Transfer β”‚ Destination β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ pokeGame β”‚ KT1VjFawYQ4JeEEAVchqaYK1NmXCENm2ufer β”‚ (Left { DROP ; PUSH string "YEAH!!!" }) β”‚ default β”‚ 0 β”‚ https://ghostnet.ecadinfra.com β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` 12. Run the user sequence on the web page: 1. Mint 1 ticket. 2. Wait for confirmation. 3. Poke a contract address. 4. Wait for confirmation. 5. Click on the button to refresh the contract list. You see that the feedback has changed to `YEAH!!!`. 13. Optional: fix the unit tests. ## Proxy pattern The goal is to have a proxy contract maintaining the application lifecycle, it is an enhancement of the previous naive solution. Deploy a completely new smart contract, but this time, the end user is not interacting directly with this contract. Instead, the proxy becomes the default entrypoint and keeps the same facing address. Init ```mermaid sequenceDiagram Admin->>Tezos: originate proxy(admin,[]) Tezos-->>Admin: proxyAddress Admin->>Tezos: originate smart contract(proxyAddress,v1) Tezos-->>Admin: contractV1Address Admin->>Proxy: upgrade([["endpoint",contractV1Address]],{new:contractV1Address}) ``` Interaction ```mermaid sequenceDiagram User->>Proxy: call("endpoint",payloadBytes) Proxy->>SmartContractV1: main("endpoint",payloadBytes) ``` Administration ```mermaid sequenceDiagram Admin->>Proxy: upgrade([["endpoint",contractV2Address]],{old:contractV1Address,new:contractV2Address}) Note right of Proxy : Check caller == admin Note right of Proxy : storage.entrypoints.set["endpoint",contractV2Address] Proxy->>SmartContractV1: main(["changeVersion",{old:contractV1Address,new:contractV2Address}]) Note left of SmartContractV1 : storage.tzip18.contractNext = contractV2Address ``` > Note: 2 location choices for the smart contract storage: > > * At proxy level: storage stays unique and immutable. > * At end-contract level: storage is new at each new version and need to be migrated. ### Pros/Cons | Pros | Cons | | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | Migration is transparent for frontend | smart contract code `Tezos.SENDER` always refers to the proxy, so you need to be careful | | if the storage is unchanged, keep the storage at proxy level without cost | If storage changes, need to migrate storage from old contract to new contract and it costs money and having storage at proxy level is not more possible | | keep same contract address | If a contract interface changed, then re-originate the proxy | | | No all of types are compatible with PACKING/UNPACKING, and type checking is lost | | | IDE or tools do not work anymore on lambda code. Michelson does not protect us from some kinds of mistakes anymore | | | Unexpected changes can cause other contract callers to fail, Interface benefits are lost | | | Harder to audit and trace, can lead to really big security nd Trust issues | | | Storing everything as bytes is limited to PACK-able types like nat, string, list, set, map | ### Implementation #### Rewrite the smart contract to make it generic 1. Rename the file `pokeGame.jsligo` to `pokeGameLambda.jsligo` , as you can have a look on it later. 2. Remove pokeGame.parameterList.jsligo. 3. Get back the original version of `pokeGame.jsligo` from previous training as it is easier to start from here. 4. Create a new file `tzip18.jsligo`. ```bash taq create contract tzip18.jsligo ``` 5. Edit the file. ```jsligo // Tzip 18 types export type tzip18 = { proxy: address, version: nat, contractPrevious: option
, contractNext: option
}; ``` This type is included on all smart contract storages to track the proxy address and the last contract version. It is used to block old smart contract instances to be called and check who can call who. 6. Get back to `pokeGame.jsligo` and import this file on the first line. ```jsligo #import "./tzip18.jsligo" "TZIP18" ``` 7. Add the type to the storage definition. ```jsligo export type storage = { pokeTraces: map, feedback: string, ticketOwnership: map>, //ticket of claims tzip18: TZIP18.tzip18 }; ``` 8. Fix all missing tzip18 fields on the storage structure in the file. ```jsligo const poke = ( _: { entrypointName: string, payload: bytes }, [pokeTraces, feedback, ticketOwnership, tzip18]: [ map, string, map>, TZIP18.tzip18 ] ): return_ => { //extract opt ticket from map const [t, tom]: [option>, map>] = Map.get_and_update( Tezos.get_source(), None() as option>, ticketOwnership ); return match(t) { when (None()): failwith("User does not have tickets => not allowed") when (Some(_t)): [ list([]) as list, { //let t burn feedback, pokeTraces: Map.add( Tezos.get_source(), { receiver: Tezos.get_self_address(), feedback: "" }, pokeTraces ), ticketOwnership: tom, tzip18, } ] }; }; @no_mutation const pokeAndGetFeedback = ( oracleAddress: address, [pokeTraces, feedback, ticketOwnership, tzip18]: [ map, string, map>, TZIP18.tzip18 ] ): return_ => { //extract opt ticket from map const [t, tom]: [option>, map>] = Map.get_and_update( Tezos.get_source(), None() as option>, ticketOwnership ); //Read the feedback view let feedbackOpt: option = Tezos.call_view("getView", "feedback", oracleAddress); return match(t) { when (None()): failwith("User does not have tickets => not allowed") when (Some(_t)): match(feedbackOpt) { when (Some(f)): do { let feedbackMessage = { receiver: oracleAddress, feedback: Option.unopt(Bytes.unpack(f) as option), }; return [ list([]) as list, { feedback, pokeTraces: Map.add( Tezos.get_source(), feedbackMessage, pokeTraces ), ticketOwnership: tom, tzip18, } ] } when (None()): failwith("Cannot find view feedback on given oracle address") } }; }; const init = ( [a, ticketCount]: [address, nat], [pokeTraces, feedback, ticketOwnership, tzip18]: [ map, string, map>, TZIP18.tzip18 ] ): return_ => { return ticketCount == (0 as nat) ? [ list([]) as list, { feedback, pokeTraces, ticketOwnership, tzip18 } ] : [ list([]) as list, { feedback, pokeTraces, ticketOwnership: Map.add( a, Option.unopt(Tezos.create_ticket("can_poke", ticketCount)), ticketOwnership ), tzip18, } ] }; ``` The view call signature is different: * It returns optional bytes. * Calling **getView** generic view exposed by the proxy. * Passing the view named **feedback** (to dispatch to the correct function once you reach the code that will be executed). * Finally, unpack the bytes result and cast it to string. With generic calls, a **unique** dispatch function has to be used and not multiple **@entry**. 9. Write a main function annotated with @entry. The parameter is a string representing the entrypoint name and some generic bytes that are required to be cast later on. In a way, compiler checks are broken, so the code is to be well-written and well-cast as earliest as possible to mitigate risks. ```jsligo @entry export const main = (action: { entrypointName: string, payload: bytes }, store: storage): return_ => { //destructure the storage to avoid DUP const { pokeTraces, feedback, ticketOwnership, tzip18 } = store; const canBeCalled: bool = match(tzip18.contractNext) { when (None()): false // I am the last version, but I cannot be called directly (or is my proxy, see later) when (Some(contract)): do { if (Tezos.get_sender() == contract) { return true; } // I am not the last but a parent contract is calling me else { return false; } } // I am not the last version and a not-parent is trying to call me }; if (Tezos.get_sender() != tzip18.proxy && ! canBeCalled) { return failwith("Only the proxy or contractNext can call this contract"); }; if (action.entrypointName == "Poke") { return poke(action, [pokeTraces, feedback, ticketOwnership, tzip18]); } else { if (action.entrypointName == "PokeAndGetFeedback") { return match(Bytes.unpack(action.payload) as option
) { when (None()): failwith("Cannot find the address parameter for PokeAndGetFeedback") when (Some(other)): pokeAndGetFeedback( other, [pokeTraces, feedback, ticketOwnership, tzip18] ) }; } else { if (action.entrypointName == "Init") { return match(Bytes.unpack(action.payload) as option<[address, nat]>) { when (None()): failwith("Cannot find the address parameter for changeVersion") when (Some(initParam)): init( [initParam[0], initParam[1]], [pokeTraces, feedback, ticketOwnership, tzip18] ) }; } else { if (action.entrypointName == "changeVersion") { return match(Bytes.unpack(action.payload) as option
) { when (None()): failwith("Cannot find the address parameter for changeVersion") when (Some(other)): changeVersion( other, [pokeTraces, feedback, ticketOwnership, tzip18] ) }; } else { return failwith("Non-existant method"); } } } } }; ``` * Start checking that only the proxy contract or the parent of this contract can call the main function. Enable this feature in case the future contract wants to run a migration *script* itself, reading from children's storage (looking at `tzip18.contractPrevious` field ). * With no more variants, the pattern matching is broken, and `if...else` statement has to be used instead. * When a payload is passed, unpack it and cast it with `(Bytes.unpack(action.payload) as option)`. It means the caller and callee agree on the payload structure for each endpoint. 10. Add the last missing function to change the version of this contract and make it obsolete (just before the main function). ```jsligo /** * Function called by a parent contract or administrator to set the current version on an old contract **/ const changeVersion = ( newAddress: address, [pokeTraces, feedback, ticketOwnership, tzip18]: [ map, string, map>, TZIP18.tzip18 ] ): return_ => { return [ list([]) as list, { pokeTraces, feedback, ticketOwnership, tzip18: { ...tzip18, contractNext: Some(newAddress) }, } ] }; ``` 11. Change the view to a generic one and do an `if...else` on `viewName` argument. ```jsligo @view const getView = (viewName: string, store: storage): bytes => { if (viewName == "feedback") { return Bytes.pack(store.feedback); } else return failwith("View " + viewName + " not found on this contract"); }; ``` 12. Change the initial storage. > Note: for the moment, initialize the proxy address to a fake KT1 address because the proxy is not yet deployed. ```jsligo #import "pokeGame.jsligo" "Contract" const default_storage : Contract.storage = { pokeTraces: Map.empty as map, feedback: "kiss", ticketOwnership: Map.empty as map>, //ticket of claims tzip18: { proxy: "KT1LXkvAPGEtdFNfFrTyBEySJvQnKrsPn4vD" as address, version: 1 as nat, contractPrevious: None() as option
, contractNext: None() as option
} }; ``` 13. Compile. ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq compile pokeGame.jsligo ``` All good. #### Write the unique proxy 1. Create a file `proxy.jsligo`. ```bash taq create contract proxy.jsligo ``` 2. Define the storage and entrypoints on it. ```jsligo export type storage = { governance: address, //admins entrypoints: big_map //interface schema map }; type _return = [list, storage]; ``` The storage: * Holds a /or several admins. * Maintains the interface schema map for all underlying entrypoints. > Note on parameters: use @entry syntax, parameters is 2 functions. > > * **call**: forward any request to the right underlying entrypoint. > * **upgrade**: admin endpoint to update the interface schema map or change smart contract version. 3. Add our missing types just above. ```jsligo export type callContract = { entrypointName: string, payload: bytes }; export type entrypointType = { method: string, addr: address }; export type entrypointOperation = { name: string, isRemoved: bool, entrypoint: option }; export type changeVersion = { oldAddr: address, newAddr: address }; ``` * **callContract**: payload from user executing an entrypoint (name+payloadBytes) * **entrypointType**: payload to be able to call an underlying contract (name+address) * **entrypointOperation**: change the entrypoint interface map (new state of the map) * **changeVersion**: change the smart contract version (old/new addresses) 4. Add the `Call`entrypoint (simple forward). (Before the main function). ```jsligo // the proxy function @entry const callContract = (param: callContract, store: storage): _return => { return match(Big_map.find_opt(param.entrypointName, store.entrypoints)) { when (None): failwith("No entrypoint found") when (Some(entry)): match( Tezos.get_contract_opt(entry.addr) as option> ) { when (None): failwith("No contract found at this address") when (Some(contract)): [ list( [ Tezos.transaction( { entrypointName: entry.method, payload: param.payload }, Tezos.get_amount(), contract ) ] ) as list, store ] } } }; ``` It gets the entrypoint to call and the payload in bytes and just forwards it to the right location. 5. Then, write the `upgrade` entrypoint. (Before the main function). ```jsligo /** * Function for administrators to update entrypoints and change current contract version **/ @entry const upgrade = ( param: [list, option], store: storage ): _return => { if (Tezos.get_sender() != store.governance) { return failwith("Permission denied") }; let [upgraded_ep_list, changeVersionOpt] = param; const update_storage = ( l: list, m: big_map ): big_map => { return match(l) { when ([]): m when ([x, ...xs]): do { let b: big_map = match(x.entrypoint) { when (None): do { if (x.isRemoved == true) { return Big_map.remove(x.name, m) } else { return m } } //mean to remove or unchanged when (Some(_ep)): do { //means to add new or unchanged if (x.isRemoved == false) { return match(x.entrypoint) { when (None): m when (Some(c)): Big_map.update(x.name, Some(c), m) } } else { return m } } }; return update_storage(xs, b) } } }; //update the entrypoint interface map const new_entrypoints: big_map = update_storage(upgraded_ep_list, store.entrypoints); //check if version needs to be changed return match(changeVersionOpt) { when (None): [list([]) as list, { ...store, entrypoints: new_entrypoints }] when (Some(change)): do { let op_change: operation = match( Tezos.get_contract_opt(change.oldAddr) as option> ) { when (None): failwith("No contract found at this address") when (Some(contract)): do { let amt = Tezos.get_amount(); let payload: address = change.newAddr; return Tezos.transaction( { entrypointName: "changeVersion", payload: Bytes.pack(payload) }, amt, contract ) } }; return [ list([op_change]) as list, { ...store, entrypoints: new_entrypoints } ] } } }; ``` * It loops over the new interface schema to update and do so. * If a **changeVersion** is required, it calls the old contract to take the new version configuration (and it disables itself). 6. The last change is to expose any view from the underlying contract and declare it at the end of the file. ```jsligo @view export const getView = (viewName: string, store: storage): bytes => { return match(Big_map.find_opt(viewName, store.entrypoints)) { when (None): failwith("View " + viewName + " not declared on this proxy") when (Some(ep)): Option.unopt( Tezos.call_view("getView", viewName, ep.addr) as option ) } }; ``` * Expose a generic view on the proxy and pass the name of the final function called on the underlying contract (as the smart contract view is not unreachable/hidden by the proxy contract). * Search for an exposed view on the interface schema to retrieve the contract address, then call the view and return the result as an *exposed* view. 7. Compile. ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq compile proxy.jsligo ``` #### Deployment 1. Edit `proxy.storageList.jsligo` to this below ( **!!! be careful to point the *governance* address to your taq default user account !!!**). ```jsligo const default_storage: Contract.storage = { governance: "tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb" as address, //admins entrypoints: Big_map.empty as big_map< string, Contract.entrypointType > //interface schema map }; ``` 2. Compile and deploy it. ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq compile proxy.jsligo taq deploy proxy.tz -e testing ``` ```logs β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Contract β”‚ Address β”‚ Alias β”‚ Balance In Mutez β”‚ Destination β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ proxy.tz β”‚ KT1Ego8vYEa4tPwkJirZfwxgJrqfmTcd8KMU β”‚ proxy β”‚ 0 β”‚ https://ghostnet.ecadinfra.com β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` Keep this **proxy address**, as you need to report it below on `tzip18.proxy` field. 3. Deploy a smart contract V1. ( :warning: Change with the **proxy address** on the file `pokeGame.storageList.jsligo` like here below ). ```jsligo #import "pokeGame.jsligo" "Contract" const default_storage: Contract.storage = { pokeTraces: Map.empty as map, feedback: "kiss", ticketOwnership: Map.empty as map>, //ticket of claims tzip18: { proxy: "KT1Ego8vYEa4tPwkJirZfwxgJrqfmTcd8KMU" as address, version: 1 as nat, contractPrevious: None() as option
, contractNext: None() as option
} }; ``` 4. Deploy the underlying V1 contract. ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq compile pokeGame.jsligo taq deploy pokeGame.tz -e testing ``` ```logs β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Contract β”‚ Address β”‚ Alias β”‚ Balance In Mutez β”‚ Destination β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ pokeGame.tz β”‚ KT1FqTZuuJCHz7Fe3J3AdpYgo2CGyhrJ6NAp β”‚ pokeGame β”‚ 0 β”‚ https://ghostnet.ecadinfra.com β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` 5. Tell the proxy that there is a first contract deployed with some interface. Edit the parameter file `proxy.parameterList.jsligo` (:warning: Change with the smart contract address on each command line on `addr` fields below). ```jsligo #import "proxy.jsligo" "Contract" const initProxyWithV1: parameter_of Contract = Upgrade( [ list( [ { name: "Poke", isRemoved: false, entrypoint: Some( { method: "Poke", addr: "KT1FqTZuuJCHz7Fe3J3AdpYgo2CGyhrJ6NAp" as address } ) }, { name: "PokeAndGetFeedback", isRemoved: false, entrypoint: Some( { method: "PokeAndGetFeedback", addr: "KT1FqTZuuJCHz7Fe3J3AdpYgo2CGyhrJ6NAp" as address } ) }, { name: "Init", isRemoved: false, entrypoint: Some( { method: "Init", addr: "KT1FqTZuuJCHz7Fe3J3AdpYgo2CGyhrJ6NAp" as address } ) }, { name: "changeVersion", isRemoved: false, entrypoint: Some( { method: "changeVersion", addr: "KT1FqTZuuJCHz7Fe3J3AdpYgo2CGyhrJ6NAp" as address } ) }, { name: "feedback", isRemoved: false, entrypoint: Some( { method: "feedback", addr: "KT1FqTZuuJCHz7Fe3J3AdpYgo2CGyhrJ6NAp" as address } ) } ] ) as list, None() as option ] ); ``` 6. Compile & Call it. ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq compile proxy.jsligo taq call proxy --param proxy.parameter.initProxyWithV1.tz -e testing ``` Output: ```logs β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Contract Alias β”‚ Contract Address β”‚ Parameter β”‚ Entrypoint β”‚ Mutez Transfer β”‚ Destination β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ proxy β”‚ KT1Ego8vYEa4tPwkJirZfwxgJrqfmTcd8KMU β”‚ (Left (Pair { Pair "Poke" False (Some (Pair "Poke" "KT1FqTZuuJCHz7Fe3J3AdpYgo2CGyhrJ6NAp")) ; β”‚ default β”‚ 0 β”‚ https://ghostnet.ecadinfra.com β”‚ β”‚ β”‚ β”‚ Pair "PokeAndGetFeedback" β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ False β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ (Some (Pair "PokeAndGetFeedback" "KT1FqTZuuJCHz7Fe3J3AdpYgo2CGyhrJ6NAp")) ; β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ Pair "Init" False (Some (Pair "Init" "KT1FqTZuuJCHz7Fe3J3AdpYgo2CGyhrJ6NAp")) ; β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ Pair "changeVersion" β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ False β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ (Some (Pair "changeVersion" "KT1FqTZuuJCHz7Fe3J3AdpYgo2CGyhrJ6NAp")) ; β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ Pair "feedback" False (Some (Pair "feedback" "KT1FqTZuuJCHz7Fe3J3AdpYgo2CGyhrJ6NAp")) } β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ None)) β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` #### Update the frontend 1. Go on the frontend side, recompile all, and generate typescript classes. ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq compile pokeGame.jsligo TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq compile proxy.jsligo taq generate types ./app/src ``` 2. Change the script to extract the proxy address instead of the contract one, edit `./app/package.json`, and replace the line of script with: ```json "dev": "jq -r -f filter.jq ../.taq/testing-state.json > .env && vite", ``` 3. Where you created a new file `filter.jq` with the below content. ```bash echo '"VITE_CONTRACT_ADDRESS=" + last(.tasks[] | select(.task == "deploy" and .output[0].contract == "proxy.tz").output[0].address)' > ./app/filter.jq ``` 4. Edit `./app/src/App.tsx` and change the contract address, display, etc ... ```typescript import { NetworkType } from '@tezos-x/octez.connect-types'; import { BeaconWallet } from '@taquito/beacon-wallet'; import { PackDataResponse } from '@taquito/rpc'; import { MichelCodecPacker, TezosToolkit } from '@taquito/taquito'; import * as api from '@tzkt/sdk-api'; import { useEffect, useState } from 'react'; import './App.css'; import ConnectButton from './ConnectWallet'; import DisconnectButton from './DisconnectWallet'; import { Storage as ContractStorage, PokeGameWalletType, } from './pokeGame.types'; import { Storage as ProxyStorage, ProxyWalletType } from './proxy.types'; import { address, bytes } from './type-aliases'; function App() { api.defaults.baseUrl = 'https://api.ghostnet.tzkt.io'; const [Tezos, setTezos] = useState( new TezosToolkit('https://ghostnet.ecadinfra.com') ); const [wallet, setWallet] = useState( new BeaconWallet({ name: 'Training', preferredNetwork: NetworkType.GHOSTNET, }) ); const [contracts, setContracts] = useState>([]); const [contractStorages, setContractStorages] = useState< Map >(new Map()); const fetchContracts = () => { (async () => { const tzktcontracts: Array = await api.contractsGetSimilar( import.meta.env.VITE_CONTRACT_ADDRESS, { includeStorage: true, sort: { desc: 'id' }, } ); setContracts(tzktcontracts); const taquitoContracts: Array = await Promise.all( tzktcontracts.map( async (tzktcontract) => (await Tezos.wallet.at(tzktcontract.address!)) as ProxyWalletType ) ); const map = new Map(); for (const c of taquitoContracts) { const s: ProxyStorage = await c.storage(); try { let firstEp: { addr: address; method: string } | undefined = await s.entrypoints.get('Poke'); if (firstEp) { let underlyingContract: PokeGameWalletType = await Tezos.wallet.at('' + firstEp!.addr); map.set(c.address, { ...s, ...(await underlyingContract.storage()), }); } else { console.log( 'proxy is not well configured ... for contract ' + c.address ); continue; } } catch (error) { console.log(error); console.log( 'final contract is not well configured ... for contract ' + c.address ); } } console.log('map', map); setContractStorages(map); })(); }; useEffect(() => { (async () => { const activeAccount = await wallet.client.getActiveAccount(); if (activeAccount) { setUserAddress(activeAccount.address); const balance = await Tezos.tz.getBalance(activeAccount.address); setUserBalance(balance.toNumber()); } })(); }, []); const [userAddress, setUserAddress] = useState(''); const [userBalance, setUserBalance] = useState(0); const [contractToPoke, setContractToPoke] = useState(''); //poke const poke = async ( e: React.MouseEvent, contract: api.Contract ) => { e.preventDefault(); let c: ProxyWalletType = await Tezos.wallet.at('' + contract.address); try { console.log('contractToPoke', contractToPoke); const p = new MichelCodecPacker(); let contractToPokeBytes: PackDataResponse = await p.packData({ data: { string: contractToPoke }, type: { prim: 'address' }, }); console.log('packed', contractToPokeBytes.packed); const op = await c.methods .callContract( 'PokeAndGetFeedback', contractToPokeBytes.packed as bytes ) .send(); await op.confirmation(); alert('Tx done'); } catch (error: any) { console.log(error); console.table(`Error: ${JSON.stringify(error, null, 2)}`); } }; //mint const mint = async ( e: React.MouseEvent, contract: api.Contract ) => { e.preventDefault(); let c: ProxyWalletType = await Tezos.wallet.at('' + contract.address); try { console.log('contractToPoke', contractToPoke); const p = new MichelCodecPacker(); let initBytes: PackDataResponse = await p.packData({ data: { prim: 'Pair', args: [{ string: userAddress }, { int: '1' }], }, type: { prim: 'Pair', args: [{ prim: 'address' }, { prim: 'nat' }] }, }); const op = await c.methods .callContract('Init', initBytes.packed as bytes) .send(); await op.confirmation(); alert('Tx done'); } catch (error: any) { console.log(error); console.table(`Error: ${JSON.stringify(error, null, 2)}`); } }; return (
I am {userAddress} with {userBalance} mutez

{contracts.map((contract) => ( ))}
address trace "contract - feedback - user" action
{contract.address} {contractStorages.get(contract.address!) !== undefined && contractStorages.get(contract.address!)!.pokeTraces ? Array.from( contractStorages .get(contract.address!)! .pokeTraces.entries() ).map( (e) => e[1].receiver + ' ' + e[1].feedback + ' ' + e[0] + ',' ) : ''} { console.log('e', e.currentTarget.value); setContractToPoke(e.currentTarget.value); }} placeholder="enter contract address here" />
); } export default App; ``` * The contract address now is pointing to the new **proxy** address. * Merge the proxy and contract storage into `ProxyStorage&ContractStorage` type definition. Fetching the contracts is appending the storage of the underlying contract to the proxy storage. * The call to expose the entrypoint is altered. As all are generic, now on the proxy side, there are only `await c.methods.callContract("my_entrypoint_name",my_packed_payload_bytes).send()` calls. 5. Run the frontend locally. ```bash cd app yarn dev ``` 6. Do all the same actions as before through the proxy. 1. Login. 2. Refresh the contract list. 3. Mint 1 ticket. 4. Wait for the confirmation popup. 5. Poke. 6. Wait for the confirmation popup. 7. Refresh the contract list. Deploy a new contract V2 and test it again. > Note: Remember that the `storage.feedback` field cannot change on any deployed smart contract because there is no exposed method to update it. > Let's change this value for the new contract instance, and call it `hello`. 7. Edit `pokeGame.storageList.jsligo` and add a new variable to it. Don't forget again to change `proxy` and `contractPrevious` by our values! ```jsligo const storageV2: Contract.storage = { pokeTraces: Map.empty as map, feedback: "hello", ticketOwnership: Map.empty as map>, tzip18: { proxy: "KT1Ego8vYEa4tPwkJirZfwxgJrqfmTcd8KMU" as address, version: 2 as nat, contractPrevious: Some( "KT1FqTZuuJCHz7Fe3J3AdpYgo2CGyhrJ6NAp" as address ) as option
, contractNext: None() as option
, }, }; ``` ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq compile pokeGame.jsligo taq deploy pokeGame.tz -e testing --storage pokeGame.storage.storageV2.tz ``` ```logs β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Contract β”‚ Address β”‚ Alias β”‚ Balance In Mutez β”‚ Destination β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ pokeGame.tz β”‚ KT1GFn9rRsMGp1JFthymxCDxQLd2xrWoXXPw β”‚ pokeGame β”‚ 0 β”‚ https://ghostnet.ecadinfra.com β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` 8. Tell the proxy that there are new V2 entrypoints and remove the V1 ones. Add a new parameter variable on `proxy.parameterList.jsligo`. Don't forget to change the `addr` values with the new contract address just above. ```jsligo const initProxyWithV2: parameter_of Contract = Upgrade( [ list( [ { name: "Poke", isRemoved: false, entrypoint: Some( { method: "Poke", addr: "KT1GFn9rRsMGp1JFthymxCDxQLd2xrWoXXPw" as address } ) }, { name: "PokeAndGetFeedback", isRemoved: false, entrypoint: Some( { method: "PokeAndGetFeedback", addr: "KT1GFn9rRsMGp1JFthymxCDxQLd2xrWoXXPw" as address } ) }, { name: "Init", isRemoved: false, entrypoint: Some( { method: "Init", addr: "KT1GFn9rRsMGp1JFthymxCDxQLd2xrWoXXPw" as address } ) }, { name: "changeVersion", isRemoved: false, entrypoint: Some( { method: "changeVersion", addr: "KT1GFn9rRsMGp1JFthymxCDxQLd2xrWoXXPw" as address } ) }, { name: "feedback", isRemoved: false, entrypoint: Some( { method: "feedback", addr: "KT1GFn9rRsMGp1JFthymxCDxQLd2xrWoXXPw" as address } ) } ] ) as list, None() as option ] ); ``` 9. Call the proxy to make the changes. ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq compile proxy.jsligo taq call proxy --param proxy.parameter.initProxyWithV2.tz -e testing ``` 10. Check the logs. ```logs β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Contract Alias β”‚ Contract Address β”‚ Parameter β”‚ Entrypoint β”‚ Mutez Transfer β”‚ Destination β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ proxy β”‚ KT1Ego8vYEa4tPwkJirZfwxgJrqfmTcd8KMU β”‚ (Left (Pair { Pair "Poke" False (Some (Pair "Poke" "KT1GFn9rRsMGp1JFthymxCDxQLd2xrWoXXPw")) ; β”‚ default β”‚ 0 β”‚ https://ghostnet.ecadinfra.com β”‚ β”‚ β”‚ β”‚ Pair "PokeAndGetFeedback" β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ False β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ (Some (Pair "PokeAndGetFeedback" "KT1GFn9rRsMGp1JFthymxCDxQLd2xrWoXXPw")) ; β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ Pair "Init" False (Some (Pair "Init" "KT1GFn9rRsMGp1JFthymxCDxQLd2xrWoXXPw")) ; β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ Pair "changeVersion" β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ False β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ (Some (Pair "changeVersion" "KT1GFn9rRsMGp1JFthymxCDxQLd2xrWoXXPw")) ; β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ Pair "feedback" False (Some (Pair "feedback" "KT1GFn9rRsMGp1JFthymxCDxQLd2xrWoXXPw")) } β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ None)) β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` 11. Back to the web app, test the flow again: 1. Refresh the contract list. 2. Mint 1 ticket. 3. Wait for the confirmation popup. 4. Poke. 5. Wait for the confirmation popup. 6. Refresh the contract list. Now, the proxy is calling the contract V2 and should return `hello` on the traces and no more `kiss`. #### Set the old smart contract as obsolete 1. Add a new parameter on `proxy.parameterList.jsligo` to force the change of version of the old contract (:warning: replace below with your addresses for V1 and V2). ```jsligo const changeVersionV1ToV2: parameter_of Contract = Upgrade( [ list([]) as list, Some( { oldAddr: "KT1FqTZuuJCHz7Fe3J3AdpYgo2CGyhrJ6NAp" as address, newAddr: "KT1GFn9rRsMGp1JFthymxCDxQLd2xrWoXXPw" as address } ) as option ] ); ``` 2. Compile. ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq compile proxy.jsligo taq call proxy --param proxy.parameter.changeVersionV1ToV2.tz -e testing ``` 3. Check logs. ```logs β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Contract Alias β”‚ Contract Address β”‚ Parameter β”‚ Entrypoint β”‚ Mutez Transfer β”‚ Destination β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ proxy β”‚ KT1Ego8vYEa4tPwkJirZfwxgJrqfmTcd8KMU β”‚ (Left (Pair {} β”‚ default β”‚ 0 β”‚ https://ghostnet.ecadinfra.com β”‚ β”‚ β”‚ β”‚ (Some (Pair "KT1FqTZuuJCHz7Fe3J3AdpYgo2CGyhrJ6NAp" "KT1GFn9rRsMGp1JFthymxCDxQLd2xrWoXXPw")))) β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` 4. Check on an indexer that the V1 `storage.tzip18.contractNext` is pointing to the next version address V2: [old V1 contract storage](https://ghostnet.tzkt.io/KT18ceGtUsNtQTk9smxQcaxAswRVkHDDKDgK/storage/). This ends the proxy pattern implementation. The old contract is no longer **runnable** and the proxy is pointing to the last version. ## Alternative: Composability Managing a monolithic smart contract like a microservice can reduce the problem, on the other side it increases complexity and application lifecycle on the OPS side. That's your tradeoff. ## Summary Now, you can upgrade deployed contracts. # Build a simple web application Estimated time: 1 hour This tutorial shows you how to create a simple web application that uses Tezos. Specifically, this application will be the user-facing web front end for a bank application that accepts deposits and returns withdrawals of test tokens. You will learn: * How to create a web application and import libraries that access Tezos * How to connect to a user's wallet * How to send a transaction to a smart contract on behalf of a user * How to get information from Tezos and show it on a web page ## Prerequisites This tutorial uses JavaScript, so it will be easier if you are familiar with JavaScript. You do not need any familiarity with any of the libraries in the tutorial, including [Taquito](https://tezostaquito.io/), a library that helps developers access Tezos. ## The tutorial application In this tutorial, you build a web application that allows users to send test tokens to a simulated bank on Tezos and withdraw them later. The application looks like this: ![Completed bank application, showing information about the user's wallet and buttons to deposit or withdraw tez](/img/tutorials/bank-app-complete.png) The application connects to a user's cryptocurrency wallet and shows the balance of that wallet in Tezos's native currency, which is referred to as tez, by the ticker symbol XTZ, or the symbol ꜩ. It provides an input field and slider for the user to select an amount of tez to deposit and a button to send the deposit transaction to Tezos. It also provides a button to withdraw the amount from Tezos. The application is based on JavaScript, so it uses several JS-based tools to build and package the application: * **[Svelte](https://svelte.dev/)** for the JavaScript framework * **[Vite](https://vitejs.dev/)** (pronounced like *veet*) to bundle the application and provide the libraries to the user's browser To access the user's wallet and run transactions on Tezos, the application uses these libraries: * **[Taquito](https://tezostaquito.io/)** to interact with the Tezos blockchain * **[octez.connect](https://github.com/trilitech/octez.connect/)** to access users' wallets The code for the completed application is in this GitHub repository: https://github.com/trilitech/tutorial-applications/tree/main/bank-tutorial. When you're ready, move to the next section to begin setting up the application. # Part 1: Setting up the application You can access Tezos through any JavaScript framework. This tutorial uses the Svelte framework, and the following steps show you how to start a Svelte application and add the Tezos-related dependencies. ## Setting up the app 1. Run this command to set up a starter Svelte application: ```bash npm create vite@latest bank-tutorial -- --template svelte ``` Answer "no" to the prompts "Use rolldown-vite (Experimental)?" and "Install with npm and start now?" 2. Run these commands to go into the new project and install its dependencies: ```bash cd bank-tutorial npm install ``` 3. Install the Tezos-related dependencies: ```bash npm install @taquito/taquito @taquito/beacon-wallet @tezos-x/octez.connect-types ``` 4. Install the `buffer`, `events`, and `vite-compatible-readable-stream` libraries: ```bash npm install --save-dev buffer events vite-compatible-readable-stream ``` 5. Update the `vite.config.js` file to the following code: ```javascript import { defineConfig, mergeConfig } from "vite"; import path from "path"; import { svelte } from "@sveltejs/vite-plugin-svelte"; export default ({ command }) => { const isBuild = command === "build"; return defineConfig({ plugins: [svelte()], define: { global: {} }, build: { target: "esnext", commonjsOptions: { transformMixedEsModules: true } }, server: { port: 4000 }, resolve: { alias: { "@tezos-x/octez.connect-types": path.resolve( path.resolve(), `./node_modules/@airgap/octez.connect-types/dist/${ isBuild ? "esm" : "cjs" }/index.js` ), // polyfills "readable-stream": "vite-compatible-readable-stream", stream: "vite-compatible-readable-stream" } } }); }; ``` This updated file includes these changes to the default Vite configuration: * It sets the `global` object to `{}` so the application can provide the value for this object in the HTML file * It includes the a path to the octez.connect SDK * It provides polyfills for `readable-stream` and `stream` 6. Update the default HTML file `index.html` to the following code: ```html Tezos Bank dApp ``` This updated file sets the `global` variable to `globalThis` and adds a buffer object to the window. The octez.connect SDK requires this configuration to run in a Vite app. 7. Replace the `src/main.js` file with this code: ```javascript import { mount } from 'svelte'; import './app.css' import App from './App.svelte'; const app = mount(App, { target: document.body }); export default app ``` ## Configuring Svelte Svelte files include several different types of code in a single file. The application's files have separate sections for JavaScript, style, and HTML code, as in this example: ```html
``` Svelte components are fully contained, which means that the style and code that you apply inside a component doesn't leak into the other components. Styles and scripts that are shared among components typically go in the `src/styles` and `scripts` or `src/scripts` folders. Follow these steps to set up the `src/App.svelte` file, which is the main component and container for other Svelte components: 1. Replace the default `src/App.svelte` file with this code: ```html
``` You will add code to connect to the user's wallet in the next section. # Part 2: Accessing wallets Accessing the user's wallet is a prerequisite for interacting with the Tezos blockchain. Accessing the wallet allows your app to see the tokens in it and to prompt the user to submit transactions, but it does not give your app direct control over the wallet. Users must still confirm all transactions in their wallet application. Using a wallet application in this way saves you from having to implement payment processing and security in your application. As you see in this section, it takes only a few lines of code to connect to a user's wallet. ## Creating and funding a wallet To use the application, you need a wallet and some tez tokens. 1. Install a Tezos-compatible wallet. Which wallet you install is up to you and whether you want to install a wallet on your computer, in a browser extension, or as a mobile app. If you don't know which one to choose, try the [Temple](https://templewallet.com/) browser extension, because then you can use it in the same browser that you are using to view the web app. For other wallets that support Tezos, see [Wallets](/using/wallets). 2. Switch the wallet to use the Shadownet testnet instead of Tezos Mainnet. Shadownet is a network for testing Tezos applications where tokens are free so you don't have to spend real currency to work with your applications. For example, for the Temple browser wallet: 1. Expand the menu at top right and then turn on **Testnet mode**, as in this picture: Setting testnet mode in Temple 2. Above the list of tokens, click the display options button: Clicking the button to open display options 3. Under **Filter by network**, expand **All Networks**. 4. Select **Shadownet**: Selecting Shadownet in the network settings Now Temple shows your token balances on the Shadownet test network. 3. From your wallet, get the address of your account, which starts with `tz1`. This is the address that applications use to work with your wallet. 4. Go to the Shadownet faucet page at https://faucet.shadownet.teztnets.com/. 5. On the faucet page, paste your wallet address into the input field labeled "Or fund any address" and click the button for the amount of tez to add to your wallet. 20 tez is enough to work with the tutorial application, and you can return to the faucet later if you need more tez. It may take a few minutes for the faucet to send the tokens and for those tokens to appear in your wallet. You can use the faucet as much as you need to get tokens on the testnet, but those tokens are worthless and cannot be used on Mainnet. ![Funding your wallet using the Shadownet Faucet](/img/tutorials/wallet-funding.png) 6. If you created a new account, initialize the account by sending any amount of tez to any other account. Before the new account can use dApps, it must send at least one transaction to Tezos. This first transaction reveals the public key that proves that transactions came from this account. If your account is new, you can send 1 tez to any other account, including your own account, via your wallet application to reveal the account. Now you have an account and funds that you can use in dApps. ## Connecting to the user's wallet In this section, you add code to connect to the user's wallet with the Taquito `TezosToolkit` and octez.connect `BeaconWallet` objects. Taquito accesses Tezos and octez.connect accesses wallets. IMPORTANT: however you design your app, it is essential to use a single instance of the `BeaconWallet` object. It is also highly recommended use a single instance of the `TezosToolkit` object. Creating multiple instances can cause problems in your app and with Taquito in general. This application keeps these objects in the `App.svelte` file because this is the only component in the application. If you add more components, you should move these objects to a separate file to maintain a single instance of them. 1. In the `src/App.svelte` file, add these imports to the `

Tezos bank dApp

{#if wallet}

The address of the connected wallet is {address}.

Its balance in tez is {balance}.

To get tez, go to https://faucet.shadownet.teztnets.com/ .

{:else} {/if}
``` ## Using the application To try the application, run `npm run dev` and open the page http://localhost:4000/ in a web browser. Because no wallet is connected, the app shows the "Connect wallet" button, as in this picture: ![The initial page of the bank dApp, showing a title and the button that connects to the user's wallet](/img/tutorials/bank-app-connect-button.png) When you click **Connect wallet**, the `connectWallet` function runs and the octez.connect toolkit opens, showing some of the types of wallets it can connect to: ![The octez.connect wallet connection popup](/img/tutorials/beacon-connect-wallet-options.png) The procedure for connecting each type of wallet is different. For example, if you are using the Temple browser extension, you click **Temple** and then **Connect now**. Then the Temple wallet shows a popup that allows you to confirm that you want to connect your wallet to the application, as in this picture: Connecting to the application in the Temple wallet Then the application runs the `getWalletBalance` function, which gets the wallet's balance in tez tokens. Because the Svelte component's variables changed, the application refreshes automatically and shows the wallet address, balance, and "Disconnect wallet" button: The application showing information about the connected wallet If you click **Disconnect wallet**, the application goes back to its initial state. Now the application can connect to user wallets. In the next section, you add code to use the wallet to get the user's approval to send transactions to Tezos. ## Design considerations Interacting with a wallet in a decentralized application is a new paradigm for many developers and users. Follow these practices to make the process easier for users: * Let users manually connect their wallets instead of prompting users to connect their wallet immediately when the app loads. Getting a wallet pop-up window before the user can see the page is annoying. Also, users may hesitate to connect a wallet before they have had time to look at and trust the application, even though connecting the wallet is harmless. * Provide a prominent button to connect and disconnect wallets. * Put the button in a predictable position, typically at the top right or left corner of the interface. * Use "Connect" as the label for the button. Avoid words like "sync" because they can have different meanings in dApps. * Display the status of the wallet clearly in the UI. You can also add information about the wallet, including token balances and the connected network for the user's convenience, as this tutorial application does. Showing information about the tokens and updating it after transactions allows the user to verify that the application is working properly. * Enable and disable functions of the application based on the status of the wallet connection. For example, if the wallet is not connected, disable buttons for transactions that require a wallet connection. Also, disable transaction buttons while transactions are pending to prevent users from making duplicate transactions. # Part 3: Sending transactions Now that the application can connect to the user's wallet, it can get the user's approval to send transactions to Tezos with that wallet. ## The tutorial smart contract This decentralized application (or dApp) uses a *smart contract* on Tezos, which is a type of program that runs on a blockchain. This contract behaves like an API, because your application calls its entrypoints to run commands. In this case, the smart contract was deployed for the purposes of this tutorial, so it is not a full-featured application. It does two things to simulate a bank: * It accepts deposits of tez tokens that users send and records how many tokens they sent. * It accepts a request to withdraw tez and sends them back to the user's wallet. The contract has two *entrypoints* for these functions, named "deposit" and "withdraw." These entrypoints are like API endpoints or functions in a program: clients can call them and pass parameters to them. However, unlike API endpoints and functions, they do not return a value. ## Steps for sending transactions Sending transactions with Taquito involves these general steps: 1. Create an object that represents the smart contract to call. 2. Disable UI elements related to the transaction to prevent the user from sending duplicate transactions. 3. Create the transaction, including specifying this information: * The entrypoint to call * The parameters to pass * The amount of tez to pass, if any * Maximum amounts for the fees for the transaction 4. Send the transaction to the user's wallet for approval. 5. Wait for the transaction to complete. 6. Update information about the user's wallet and other details in the UI based on the result of the transaction. 7. Enable UI elements that were disabled during the transaction. ## Making a deposit transaction Follow these steps to set up the application to send transactions to the deposit entrypoint: 1. In the `App.svelte` file, add the address of the contract as a constant with the other constants in the `

Tezos bank dApp

{#if wallet}

The address of the connected wallet is {address}.

Its balance in tez is {balance}.

To get tez, go to https://faucet.shadownet.teztnets.com/ .

Deposit tez:

Withdraw tez:

{:else} {/if}
``` # Part 4: Getting information In this section, you improve the user experience of the application by providing information from Tezos on the page. Specifically, you show the amount of tez that the user has stored in the smart contract and that is available to withdraw. Your app can do this because information on Tezos is public, including the code of smart contracts and their data storage. In this case, the contract's storage is a data type called a big-map. It maps account addresses with a number that indicates the amount of tez that address has deposited. Your app can query that amount by getting the contract's storage and looking up the value for the user's account. You can look up the storage manually by going to a block explorer and going to the Storage tab. For example, the [TzKt block explorer](https://shadownet.tzkt.io) shows the storage for this contract like this: ![The block explorer, showing the storage for the contract in one big-map object](/img/tutorials/bank-app-block-explorer-storage.png) You can expand the big-map object and search for the record that is related to your account address to see how much tez you have deposited. ## Accessing the contract storage Your application can use the Taquito library to access the storage for the contract and look up the user's balance in the contract: 1. In the `App.svelte` file, in the ` Create NFTs ``` This updated file sets the `global` variable to `globalThis` and adds a buffer object to the window. The application requires this configuration to use the octez.connect SDK to connect to wallets in a Vite app. 7. In the `src/main.js` file, import the style sheets by replacing the default code of the file with this code: ```javascript import './app.css' import { mount } from 'svelte'; import App from './App.svelte' const app = mount(App, { target: document.body }); export default app ``` This code targets the `body` tag to inject the HTML produced by JavaScript instead of a `div` tag inside the `body` tag as Svelte apps do by default. Your applications can target any tag on the page. ## File structure The structure of the tutorial application looks like this: ``` - src - assets - lib - app.css - App.svelte - main.js - index.html - jsconfig.json - package-lock.json - package.json - svelte.config.js - vite.config.js ``` Here are descriptions for each of these files: * **assets** -> Contains the favicon and other static files such as images for the application. * **lib** -> Contains the components that make up the app interface: * **app.css** -> Contains global styles that apply to the entire app. * **App.svelte** -> The entrypoint of the application, which contains the components that are bundled into the final application. * **main.js** -> Where the JavaScript for the app is bundled before being injected into the HTML file. * **index.html** -> Contains the root element where the Svelte app gets attached. * **jsconfig.json** -> Configuration options for JavaScript. * **package.json** -> Contains metadata about the project like its name, version, and dependencies. * **svelte.config.js** -> Configuration file for the Svelte application. * **vite.config.js** -> Used to customize Vite's behavior, including defining plugins, setting up aliases, and more. ## Configuring the Svelte application Svelte files include several different types of code in a single file. This example page has separate sections for JavaScript, style, and HTML code: ```html
``` Svelte components are fully contained, which means that the style and JS/TS code that you apply inside a component doesn't leak into the other components of your app. Styles and scripts that are shared among components typically go in the `src/styles` and `scripts` or `src/scripts` folders. Follow these steps to set up the `src/App.svelte` file, which is the container for the other Svelte components: 1. In the `App.svelte` file, replace the default `
` section with this code to set up a title for the interface: ```html

Simple NFT dApp

``` You will add elements to the web application interface later. 2. Replace the default ` ``` Later, you can add styles to this section or the shared CSS files. 3. Remove the default JavaScript section and replace it with this code, which imports the libraries and components that the app uses: ```html ``` The imports include these elements: * `BeaconWallet`: This class provides a user interface for connecting to wallets, ensuring that users can securely sign transactions and call smart contracts * `NetworkType`: An enumeration that lists the different types of networks on the Tezos blockchain (including Mainnet and various testnets) * `TezosToolkit`: This is the base class for Taquito, which gives you access to most of its Tezos-related features * `MichelsonMap`: This class represents the Michelson map data type, which Tezos uses to store data, including mapping the ownership and metadata for the NFTs that the application creates * `stringToBytes`: A utility that converts strings to bytes to store in the token metadata 4. In the same `