# 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 import WalletSetup from '@site/docs/conrefs/smart-contract-tutorial-wallet-setup.md'; 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 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 Custom, as shown in this picture: :::warning You must *not* select the default Tezos Testnet because it will point you to Ghostnet, which is a legacy test network, soon to be decommissioned. A future release of LIGO will retarget this default choice to Shadownet. ::: ![Selecting Custom in the list of networks](/img/tutorials/ligo-online-ide-select-custom.png) Then in the newly opened window "Custom network" click on "New connection", and in the newly opened subwindow "New Custom Network Connection" fill in `Shadownet` for the Name and `https://rpc.shadownet.teztnets.com` for the Url of node rpc, as shown in this picture: ![Defining Shadownet as a custom network](/img/tutorials/ligo-online-ide-define-shadownet.png) then click on "Check network" to update the Network info, then click on "Add network". Back in window "Custom network", click on "Connect", and you're now connected to the Shadownet testnet. Note that you only have to add Shadownet once, because the next times it will appear as a choice in the "Network" menu, in the "Others" section, as shown in this picture: ![Selecting Shadownet in the list of networks](/img/tutorials/ligo-online-ide-select-shadownet.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 import WalletSetup from '@site/docs/conrefs/smart-contract-tutorial-wallet-setup.md'; 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 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 Custom, as shown in this picture: :::warning You must *not* select the default Tezos Testnet because it will point you to Ghostnet, which is a legacy test network, soon to be decommissioned. A future release of LIGO will retarget this default choice to Shadownet. ::: ![Selecting Custom in the list of networks](/img/tutorials/ligo-online-ide-select-custom.png) Then in the newly opened window "Custom network" click on "New connection", and in the newly opened subwindow "New Custom Network Connection" fill in `Shadownet` for the Name and `https://rpc.shadownet.teztnets.com` for the Url of node rpc, as shown in this picture: ![Defining Shadownet as a custom network](/img/tutorials/ligo-online-ide-define-shadownet.png) then click on "Check network" to update the Network info, then click on "Add network". Back in window "Custom network", click on "Connect", and you're now connected to the Shadownet testnet. Note that you only have to add Shadownet once, because the next times it will appear as a choice in the "Network" menu, in the "Others" section, as shown in this picture: ![Selecting Shadownet in the list of networks](/img/tutorials/ligo-online-ide-select-shadownet.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/) # 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/@tezos-x/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/KT1HtZfNKVcgPYCTuVPKm3cjEVAC4CKYrfjX) 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 (but right now you won't find your address because you have just withdrawn all your balance). ## 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 Follow these steps to set up the `src/App.svelte` file, which is the container for the other Svelte components: 1. Replace the default `src/App.svelte` file with this content: ```html
``` Svelte files can include several different types of code in a single file. The above template page has separate sections for JavaScript, style, and HTML code. 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. 2. 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. 3. Replace the default ` ``` Later, you can add styles to this section or the shared CSS files. 4. 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 5. In the same ` ``` 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. # Run a Tezos baker in 5 steps Estimated time: 90 minutes plus time to wait for attestation rights if you are setting up a new baker As described in [Nodes](/architecture/nodes), Tezos nodes are peer-to-peer programs that run the Tezos network. Anyone can run a node, and they might do so for many different reasons, including: * Running nodes makes the Tezos network resilient and secure * Public nodes may have rate limits, so running your own node allows you to send unlimited requests to it to get information about Tezos or to send transactions from your dApps * Running a node is part of being a baker and receiving the rewards for baking This tutorial covers setting up a Tezos node as a baker, which includes running these processes: * An Octez node, sometimes referred to as a Tezos node * A baker daemon * A [DAL](/architecture/data-availability-layer) node :::note If you want to use a Ledger hardware wallet to secure your keys, see [Bake using a Ledger device](/tutorials/bake-with-ledger). ::: ## Why is a DAL node needed? The Tezos data availability layer (DAL) is a peer-to-peer network that Tezos Smart Rollups can use to fetch data securely. The DAL is a key component for the scalability and bandwidth of Tezos and it's important for bakers to run DAL nodes along with their layer 1 nodes. When users and dApps submit data to the DAL, bakers use DAL nodes to verify that the data is available. Then the bakers attest that the data is available. Smart Rollup nodes can retrieve the data from DAL nodes only when enough bakers have attested that the data is available. Therefore, the DAL needs bakers who run layer 1 nodes, attesters, and DAL nodes. Starting with the Rio upgrade, 10% of bakers' rewards are tied to their participation in attesting DAL data. ## Do you already run a baker? For current bakers, it's a straightforward process to add a DAL node. If you are familiar with running a node and baker, you can add a DAL node to your existing setup by following the instructions in [Running a DAL attester node](https://octez.tezos.com/docs/shell/dal_run.html). ## Do you need to migrate to `tz4` consensus and companion keys? If you already run a baker and DAL node and want to transition to `tz4` consensus and companion keys, which allow you to participate in aggregated attestations, see [Changing the consensus key or DAL companion key](/tutorials/join-dal-baker/verify-rights#changing-the-consensus-key-or-dal-companion-key). ## Running a baker and DAL node from start to finish This guide covers the process of running a node, baker, and DAL node from start to finish, accessible for Tezos users with no prior experience in baking or running nodes. This guide walks you through how to join Shadownet as a baker and attest the publication of data on the DAL network on Shadownet. The steps for participating on any other network, including Tezos Mainnet, are similar. :::note Attestation rights delay Bakers need attestation rights to attest that data is available on the DAL. Depending on the network, it takes time for bakers to get attestation rights. The delay on Mainnet is about 2 days, so you do setup work, wait 2 days for attestation rights, and verify that your DAL node and baker are working properly. If you don't want to wait that long, you can use Weeklynet, where the delay is about an hour. However, to use Weeklynet, you must use a specific version of the Octez suite. You must also be aware that the network completely resets and moves to a new version of the Octez suite every Wednesday. For information about using Weeklynet, see [Testing on testnets](/developing/testnets). ::: ## Diagram In this guide, you set up the Octez client and several Octez daemons, including a layer 1 node, a baker, and a DAL node. The following diagram shows these daemons with a blue background: ![A diagram of the DAL architecture, with the daemons that you create in this guide highlighted](/img/tutorials/join-dal-baker-overview.png) ## Prerequisites To run the Octez daemons persistently, you need a cloud-based computer or a computer that stays running constantly. For other system requirements, see the documentation for the [latest release of the Octez suite](https://octez.tezos.com/releases/) (search for section "Minimal hardware specifications"). ## Other options for running a baker These instructions are for baking with the Octez suite programs. Other tools can help you set up a baker, but they are not covered in these instructions. Here are some of these tools: * [BakeBuddy and Ledger Nano](https://www.bakebuddy.xyz/): An intuitive plug-and-use method for setting up a node and baker * [Kiln and Ledger Nano](https://gitlab.com/tezos-kiln/kiln): An intuitive plug-and-use method for setting up a node and baker * [Signatory remote signer](https://github.com/ecadlabs/signatory) ## References * For an overview of the DAL, see [Data Availability Layer](/architecture/data-availability-layer). * For technical information about the DAL, see [Data-Availability Layer](https://octez.tezos.com/docs/shell/dal.html) in the Octez documentation. ## Getting started To get started, go to [Step 1: Run an Octez node](/tutorials/join-dal-baker/run-node). # Step 1: Run an Octez node The first thing you need is a Tezos layer 1 node, which is an instance of the `octez-node` program and part of the Octez suite of programs. ## Installing Octez The version of Octez to use depends on the Tezos network that you are using. * For Mainnet or Shadownet, install the most recent release of Octez, including `octez-client`, `octez-node`, `octez-dal-node`, `octez-baker`, and `octez-accuser`: * On MacOS, we provide a [Homebrew](https://brew.sh/) formula to install Octez. First, make sure Homebrew is installed on your system. (If you ever get errors during the steps below, you may need to reinstall Homebrew using its latest official installation script.) Then, run the following commands (replacing, if you want, `octez-user` and `octeztap` with the desired user name and tap name, respectively): ```bash # Download formula: curl -q "https://packages.nomadic-labs.com/homebrew/Formula/octez.rb" -O # Create a local tap: brew tap-new octez-user/octeztap # Move formula to the newly created tap: mv octez.rb $(brew --repository)/Library/Taps/octez-user/homebrew-octeztap/Formula/ # Install formula from tap brew install octez-user/octeztap/octez ``` * On Linux and Windows WSL, download and install the built binaries from the [Octez release page](https://octez.tezos.com/releases/), as in this example for Ubuntu: ```bash wget https://octez.tezos.com/releases/octez-v23.2/binaries/x86_64/octez-v23.2.tar.gz tar xf octez-v23.2.tar.gz sudo cp octez/octez* /usr/local/bin/ ``` Check the dedicated [Octez release page](https://octez.tezos.com/releases/) for other binaries built for other architectures. * For Weeklynet, look up the necessary version of Octez at https://teztnets.com/weeklynet-about and install it with the instructions there. For more installation options, see [Installing Octez](https://octez.tezos.com/docs/introduction/howtoget.html) in the Octez documentation. If you build from source, you can use the `latest-release` branch to work with Shadownet. ## Data directories and configuration file locations The Octez node, DAL node, baker, and some other Octez daemons store data in two places: * A *data directory*, which stores raw information, such as the context (for the Octez node) or the DAL data (for the DAL node). * A *configuration file*, which lists details about the daemon's configuration, such as the network and bootstrap peers that it connects to. By default, the configuration file is stored in the data directory. For example, by default, the Octez node stores its data directory at `$HOME/.tezos-node/` and its configuration file at `$HOME/.tezos-node/config.json`. When you run daemons, you must ensure that each command points the daemon to the correct locations. For example, if you initialize the Octez node with the `config init` command and specify one location and then run it with the `run` command with a different location, the node might not not run as you intend. There are multiple ways to manage these locations: * Use the default locations * Pass the locations in the `--data-dir` and/or `--config-file` arguments in each command that you run * In some cases, you can set environment variables to point to data directory locations You can also set the location of the data directory in the `data-dir` field of the configuration file and use only the `--config-file` argument. Similarly, you can set the location of the data directory with the `--data-dir` argument or environment variable and the daemon stores the configuration file in that directory by default. This table shows the defaults and the ways to set these locations for the main daemons that you use in this tutorial: {
Daemon Data Default Command-line argument Environment variable
Data directory $HOME/.tezos-client --data-dir TEZOS_CLIENT_DIR
Octez client Configuration file $HOME/.tezos-client/config --config-file N/A
Data directory $HOME/.tezos-node --data-dir TEZOS_NODE_DIR
Octez node Configuration file $HOME/.tezos-node/config.json --config-file N/A
Data directory $HOME/.tezos-dal-node --data-dir N/A
Octez DAL node Configuration file $HOME/.tezos-dal-node/config.json --config-file N/A
Data directory $HOME/.tezos-client (Shared with the Octez client) --data-dir TEZOS_CLIENT_DIR
Octez baker Configuration file $HOME/.tezos-client/config (Shared with the Octez client) --config-file N/A
} :::important Throughout this tutorial, make sure that you are consistent with the locations of the configuration file and data directory for each daemon. ::: ## Running the layer 1 node 1. Ensure that the port on which the node listens for connections from peer nodes (by default, 9732) is accessible from outside its system. You may need to adapt your firewall rules or set up network address translation (NAT) to direct external traffic to the node. 2. Initialize the Octez node for the network. For example, to initialize it for Shadownet, run this command: ```bash octez-node config init --network https://teztnets.com/shadownet ``` Remember to be aware of the locations described in [Data directories and configuration file locations](#data-directories-and-configuration-file-locations). 3. Download a rolling snapshot of the network from https://snapshot.tzinit.org based on the instructions on that site. For example, the command to download a Shadownet snapshot from the European servers might look like this: ```bash wget -O snapshot_file https://snapshots.eu.tzinit.org/shadownet/rolling ``` 4. Load the snapshot in the node by running this command: ```bash octez-node snapshot import snapshot_file ``` If you get an error that says that the data directory is invalid, you have selected a data directory that has been used. Delete the files in the directory or use a new directory. 5. Install the Zcash parameters as described [Install Zcash Parameters](https://octez.tezos.com/docs/introduction/howtobuild.html#install-zcash-parameters) in the Octez documentation. 6. Start the node: ``` octez-node run --rpc-addr 127.0.0.1:8732 ``` You can add the argument `--log-output="$HOME/octez-node.log"` to redirect its output in a log file. At first launch, the node generates a fresh identity file used to identify itself on the network. Then it bootstraps the chain, which takes a variable amount of time depending on how many blocks need to be loaded. 7. Make sure the Octez client uses your node by running this command: ```bash octez-client -E http://localhost:8732 config update ``` If you see an error that says "Failed to acquire the protocol version from the node," ensure that your node is running and verify that the host name and port in the `config update` command are correct. 8. Wait for your node to bootstrap by running this command: ```bash octez-client bootstrapped ``` The client waits until it is connected and the node is running at the current level. When it is connected and the node is updated, the command prints the message `Node is bootstrapped`. The time it takes depends on how many blocks the node must retrieve to catch up from the snapshot to the current head block. 9. Optional: Hide the Octez client's network warning message by running this command: ```bash export TEZOS_CLIENT_UNSAFE_DISABLE_DISCLAIMER=y ``` This command suppresses the message that your instance of the Octez client is not using Mainnet. 10. Ensure that the node runs persistently. Look up how to run programs persistently in the documentation for your operating system. You can also refer to [Setting up Octez Services](https://octez.tezos.com/docs/introduction/services.html) in the Octez documentation. For example, if your operating system uses the `systemd` software suite, your service file might look like this example: ```systemd title="/etc/systemd/system/octez-node.service" [Unit] Description=Octez node Wants=network-online.target After=network-online.target [Install] WantedBy=multi-user.target [Service] Type=simple User=tezos ExecStart=octez-node run --rpc-addr 127.0.0.1:8732 --data-dir $HOME/.tezos-node WorkingDirectory=/opt/octez-node Restart=on-failure RestartSec=5 StandardOutput=append:/opt/octez-node.log StandardError=append:/opt/octez-node.log SyslogIdentifier=%n ``` If you name this service file `/etc/systemd/system/octez-node.service`, you can start it by running these commands: ```bash sudo systemctl daemon-reload sudo systemctl start octez-node.service ``` You can stop it by running this command: ```bash sudo systemctl stop octez-node.service ``` The `systemd` software suite uses the `journalctl` program for logging, so you can use it to monitor the node and the other Octez daemons you run. For example, this command prints the log of the Octez node service as it is updated, similar to the `tail -f` command: ```bash journalctl --follow --unit=octez-node.service ``` The `journalctl` program has options that let you search logs during time periods. For example, this command shows log entries between two times: ```bash journalctl --unit=octez-node.service --since "20 minutes ago" --until "60 seconds ago" ``` For more information about logging, see the documentation for the `journalctl` program. 11. Optional: When the node has bootstrapped and caught up with the current head block, you can delete the snapshot file to save space. In the meantime, you can continue the baking infrastructure while the node is bootstrapping. Continue to [Step 2: Set up baker accounts](/tutorials/join-dal-baker/prepare-account). # Step 2: Set up baker accounts In this section you use the Octez client to set up three accounts for your baker: * The baker key itself (also called the manager key) registers as a delegate, manages the baker's stake, participates in governance, and manages so-called auxiliary keys (consensus keys and companion keys). * The consensus key is the key that the baker uses to sign *consensus operations* (preattestations and attestations) and blocks. * The DAL companion key is the key that the DAL node uses to sign DAL data attestations. ## Why set up a consensus key? Using a separate consensus key is not required but it is good security practice, because it enforces separation of concerns. Signing consensus operations incurs no fees, so you can set up a consensus key with no tez. You can generate and use this key on the machine that runs the baker and keep the baker key with your staked tez in a more secure location to reduce risk to your funds. Also, in case you want to run the baker from a new machine, this avoids you having to move your private baker key between machines, which is inherently dangerous. If the consensus key is compromised or lost, you can create a new consensus key and switch the baker to it without changing how your tez is staked and delegated and without moving your delegators and stakers to a new account. This can also avoid transferring or backing up the consensus key. You can store your consensus key in a Key Management System (KMS) or Hardware Security Module (HSM) where no one has access to its private key. Alternatively, you can generate, keep, and use your consensus key on a specialized hardware device called the [Tezos RPI BLS Remote Signer](https://gitlab.com/nomadic-labs/tezos-rpi-bls-signer). In any case, take care to protect the consensus key. The active consensus key can be used to drain the spendable tez balance from the baker key. Also, a compromised consensus key may be misused for double baking, which is punished by slashing funds. For these reasons, you must keep the consensus key secure like all other keys. If your consensus key is compromised (or you suspect it might be), rotate it immediately as described in [Changing the consensus key](/tutorials/join-dal-baker/verify-rights#changing-the-consensus-key-or-dal-companion-key). Because the activation delay for a new consensus key is shorter than the unstaking delay, you can change the consensus key before a malicious user can use the compromised key to drain the staked funds. In general, it is a good practice to rotate consensus keys often, and bakers are advised to systematically rotate consensus keys when performing a substantial unstake operation. Consensus keys also allow you to deploy a replacement baker setup quickly if your main baker setup fails and you cannot access it. For example, if you are traveling when your baker setup fails and you don't have quick access to the physical hardware or signer, you can switch consensus keys and deploy a new server in the cloud to run until you have physical access to the server. For more information about consensus keys, see [Consensus key](https://octez.tezos.com/docs/user/key-management.html#consensus-key) in the Octez documentation. :::note Aggregated attestations Starting with protocol Tallinn, bakers can be more efficient by aggregating attestations from different bakers into a single operation. To take advantage of aggregated attestations, the key that bakers sign attestations with (the consensus key, or if a consensus key is not used, the baker key) must be a `tz4` key, which is generated with the BLS signature scheme. To generate a `tz4` key, pass the argument `-s bls` to the Octez client, as in this code: ```bash octez-client gen keys my_consensus -s bls ``` ::: ## Why set up a DAL companion key? A DAL companion key is required if you are using a `tz4` consensus key. If you are not using a `tz4` consensus key, using a DAL companion key is not required but, like using a consensus key, it allows the DAL node to aggregate attestations to make them more efficient. DAL companion keys must be `tz4` keys, generated with the argument `-s bls`, as in this example: ```bash octez-client gen keys my_companion -s bls ``` The DAL companion key does not need any tez to oprate and cannot drain funds from the baker key. For more information about companion keys, see [Companion key](https://octez.tezos.com/docs/user/key-management.html#companion-key) in the Octez documentation. ## Creating the accounts In this section, you use the Octez client to create these accounts and set them up for baking. 1. Create or import an account in the Octez client to be the baker account (sometimes called the "manager" account). The simplest way to get an account is to use the Octez client to randomly generate an account. This command creates an account and associates it with the `my_baker` alias: ```bash octez-client gen keys my_baker ``` If you do not intend to use a consensus key, pass the `-s bls` argument to generate a `tz4` baker key: ```bash octez-client gen keys my_baker -s bls ``` You can get the address of the generated account with this command: ```bash octez-client show address my_baker ``` You can check the liquid balance of the account with this command: ```bash octez-client get balance for my_baker ``` 2. Ensure your account has at least 6,000 tez to stake, plus a small liquid amount for transaction fees. If you are using a testnet and need more tez, you can get tez from the testnet faucet. For example, if you are using Shadownet, use the Shadownet faucet linked from https://teztnets.com/shadownet-about to send tez to the baker account. Running a baker requires staking at least 6,000 tez, but the more tez it stakes, the more rights it gets and the less time it has to wait to produce blocks and make attestations. However, be aware that getting large amounts of tez from the faucet may take a long time (sometimes more than one hour) to prevent abuse of the faucet. Consequently, some large requests may time out or fail and need to be resubmitted. When the account receives its tez, it owns enough stake to bake but has still no consensus or DAL rights because it has not declared its intention to become a baker. 3. (Recommended) Set up a separate account to be the consensus key. :::note Tezos RPI BLS Remote Signer If you use the [Tezos RPI BLS Remote Signer](https://gitlab.com/nomadic-labs/tezos-rpi-bls-signer): instead of Steps 3 and 4 in the current page, follow the instructions in that repository for generating and importing the auxiliary keys (both the consensus key and the companion key) and resume at Step 5 below. ::: This command creates an account and associates it with the `consensus_key` alias: ```bash octez-client gen keys consensus_key -s bls ``` The consensus key does not need any tez. 4. (Recommended) Set up a separate account to be the DAL companion key. This command creates an account and associates it with the `companion_key` alias: ```bash octez-client gen keys companion_key -s bls ``` The companion key does not need any tez. 5. Register the baker account as a delegate and set its consensus key and DAL companion keys (if you want to use them) by running the following command: ```bash octez-client register key my_baker as delegate --consensus-key consensus_key --companion-key companion_key ``` If you are not using a consensus key and/or a companion key, omit the `--consensus-key` and/or `--companion-key` arguments. 6. Stake at least 6,000 tez with the account, saving a small liquid amount for transaction fees. Staked tez is temporarily frozen and cannot be spent, so you need some unstaked tez to pay transaction fees. Pass the amount to the `stake` command, as in this example: ```bash octez-client stake 6000 for my_baker ``` You can check how much you have staked by running this command: ```bash octez-client get staked balance for my_baker ``` You can also check the full balance of the account (staked + non-staked) with this command: ```bash octez-client get full balance for my_baker ``` Now the baker account has staked enough tez to earn the right to make attestations, including attestations that data is available on the DAL. If you set up a consensus key, that key is authorized to sign consensus operations on behalf of the baker account. However, the accounts do not receive these rights until a certain amount of time has passed. While you wait for attestation rights, continue to [Step 3: Run an Octez DAL node](/tutorials/join-dal-baker/run-dal-node). # Step 3: Run an Octez DAL node The DAL node is responsible for temporarily storing data and providing it to bakers and Smart Rollups. As described in [Run a Tezos baker in 5 steps](/tutorials/join-dal-baker), 10% of baker rewards are tied to their participation in attesting DAL data. Follow these steps to run the DAL node: 1. Ensure that the port on which the DAL node listens for connections from peer nodes (by default, 11732) is accessible from outside its system. You might need to adapt your firewall rules or set up network address translation (NAT) to direct external traffic to the DAL node. For more information, see [Running a DAL attester node](https://octez.tezos.com/docs/shell/dal_run.html) in the Octez documentation. 2. Initialize the DAL node by running its `config init` command, passing: * The address of your `octez-node` instance in the `--endpoint` argument (replace `localhost` with the URL of the Octez node) * Your baker's key address **(not the consensus key or DAL companion key)**, in the `--attester-profiles` argument, replacing `` with the key ```bash octez-dal-node config init --endpoint http://localhost:8732 --attester-profiles= ``` :::note For ``, you cannot use the `my_baker` alias from the Octez client as in the previous section, so you must specify the baker's key address (not its consensus key or DAL companion key) explicitly. ::: Remember to be aware of the locations described in [Data directories and configuration file locations](/tutorials/join-dal-baker/run-node#data-directories-and-configuration-file-locations). 3. Start the DAL node by running this command: ```bash octez-dal-node run ``` You can append `>>"$HOME/octez-dal-node.log" 2>&1` to redirect its output in a log file. This, too, may take some time to launch the first time because it needs to generate a new identity file, this time for the DAL network. :::note If you need to change the address or port that the DAL node listens for connections to other nodes on, pass the `--public-addr` argument. By default, it listens on port 11732 on all available network interfaces, equivalent to `--public-addr 0.0.0.0:11732`. ::: 4. Verify that the DAL node is connected to the DAL network. For this, you can run the following command: ```bash curl http://localhost:10732/p2p/points/info ``` where `10732` is the default port on which the DAL node serves RPC calls. :::note * You can override the address and the port at which the RPC server of the DAL node can be reached with the `--rpc-addr` argument. * You can replace `localhost` with the hostname where the DAL node runs, if you are on a different host. ::: The response lists the network connections that the DAL node has, as in this example: ```json [ { "point": "46.137.127.32:11732", "info": { "trusted": true, "state": { "event_kind": "running", "p2p_peer_id": "idrpUzezw7VJ4NU6phQYuxh88RiU1t" }, "p2p_peer_id": "idrpUzezw7VJ4NU6phQYuxh88RiU1t", "last_established_connection": [ "idrpUzezw7VJ4NU6phQYuxh88RiU1t", "2024-10-24T15:02:31.549-00:00" ], "last_seen": [ "idrpUzezw7VJ4NU6phQYuxh88RiU1t", "2024-10-24T15:02:31.549-00:00" ] } }, { "point": "52.31.26.230:11732", "info": { "trusted": true, "state": { "event_kind": "running", "p2p_peer_id": "idqrcQybXbKwWk42bn1XjeZ33xgduC" }, "p2p_peer_id": "idqrcQybXbKwWk42bn1XjeZ33xgduC", "last_established_connection": [ "idqrcQybXbKwWk42bn1XjeZ33xgduC", "2024-10-24T15:02:31.666-00:00" ], "last_seen": [ "idqrcQybXbKwWk42bn1XjeZ33xgduC", "2024-10-24T15:02:31.666-00:00" ] } } ] ``` It may take a few minutes for the node to connect to the DAL network. You can also verify that the DAL node is connected by viewing its log. When the node is bootstrapped it logs messages that look like this: ``` Aug 12 17:44:19.985: started tracking layer 1's node Aug 12 17:44:24.418: layer 1 node's block at level 7538687, round 0 is final Aug 12 17:44:29.328: layer 1 node's block at level 7538688, round 0 is final ``` The DAL node waits for blocks to be finalized, so this log lags 2 blocks behind the layer 1 node's log. Now the DAL node is connected to the DAL network but it is not yet receiving data. 5. Ensure that the DAL node runs persistently. Look up how to run programs persistently in the documentation for your operating system. You can also refer to [Setting up Octez Services](https://octez.tezos.com/docs/introduction/services.html) in the Octez documentation. For example, if your operating system uses the `systemd` software suite, your service file might look like this example: ```systemd [Unit] Description=Octez DAL node Wants=network-online.target After=network-online.target Requires=octez-node.service [Install] WantedBy=multi-user.target RequiredBy=octez-baker.service [Service] Type=simple User=tezos ExecStart=/usr/bin/octez-dal-node run --data-dir $HOME/.tezos-dal-node WorkingDirectory=$HOME/.tezos-dal-node Restart=on-failure RestartSec=5 StandardOutput=append:/opt/octez-dal-node.log StandardError=append:/opt/octez-dal-node.log SyslogIdentifier=%n ``` Now that you have a DAL node running, you can start a baking daemon that uses that DAL node. Continue to [Step 4: Run an Octez baking daemon](/tutorials/join-dal-baker/run-baker). # Step 4: Run an Octez baking daemon Now that you have a layer 1 node and a DAL node, you can run a baking daemon that can create blocks and attests to DAL data. If you already have a baking daemon, you can restart it to connect to the DAL node. :::warning Run one baker per account Be sure to run only one baking daemon per Tezos account. If you run more than one, you risk double-baking or double-attesting and being slashed. ::: :::note Single baker and accuser executables Versions of the Octez suite prior to version 23 provided separate baker binaries for each protocol. Starting in version 23, releases of Octez include a single baker binary named `octez-baker` and a single accuser binary named `octez-accuser` that can be used with any supported protocol, including the current protocol and sometimes a proposed or voted upcoming protocol. Therefore, you no longer need to switch baker and accuser daemons when the protocol changes or run separate daemons for the current protocol and an upcoming protocol. The `octez-baker` and `octez-accuser` daemons are aware of protocol changes and switch to new protocols automatically. Instead, you update them when new versions of the Octez suite become available, just like the other components of the Octez suite. ::: 1. Optional: Set up a remote signer to secure the keys that the baker uses as described in [Signer](https://octez.tezos.com/docs/user/key-management.html#signer) in the Octez documentation. 2. Run a baking daemon with the following arguments: * Use the consensus key, not the baker key, if you are using a consensus key * Include the DAL companion key if you are using it * Pass the URL to your DAL node with the `--dal-node` argument * Pass the `--liquidity-baking-toggle-vote` argument; for more information, see [Liquidity baking](https://octez.tezos.com/docs/active/liquidity_baking.html) in the Octez documentation For example: ```bash octez-baker run with local node "$HOME/.tezos-node" consensus_key companion_key --liquidity-baking-toggle-vote pass --dal-node http://127.0.0.1:10732 ``` You can append `>>"$HOME/octez-baker.log" 2>&1` to redirect its output in a log file. Remember to be aware of the locations described in [Data directories and configuration file locations](/tutorials/join-dal-baker/run-node#data-directories-and-configuration-file-locations). 3. Ensure that the baker runs persistently. Look up how to run programs persistently in the documentation for your operating system. For example, if your operating system uses the `systemd` software suite, your service file might look like this example, which uses `consensus_key` and `companion_key` as the names of the consensus key and DAL companion key: ```systemd title="/etc/systemd/system/octez-baker.service" [Unit] Description=Octez baker Wants=network-online.target After=network-online.target Requires=octez-node.service [Install] WantedBy=multi-user.target [Service] Type=simple User=tezos ExecStart=octez-baker run with local node "$HOME/.tezos-node" consensus_key companion_key --liquidity-baking-toggle-vote pass --dal-node http://127.0.0.1:10732 WorkingDirectory=/opt/octez-baker Restart=on-failure RestartSec=5 StandardOutput=append:/opt/octez-baker.log StandardError=append:/opt/octez-baker.log SyslogIdentifier=%n ``` If you name this service file `/etc/systemd/system/octez-baker.service`, you can start it by running these commands: ```bash sudo systemctl daemon-reload sudo systemctl start octez-baker.service ``` You can stop it by running this command: ```bash sudo systemctl stop octez-baker.service ``` 4. Verify that the baker is connected to the DAL network by running this command: ```bash curl http://localhost:10732/p2p/gossipsub/topics ``` This command prints information about the DAL data that this baker is assigned to attest. In particular, it prints a list of *topics*, which are information feeds that DAL participants can subscribe to. Each topic identifies a specific baker, a slot, and a list of shards that the baker must attest for that slot. The baker daemon automatically asks the DAL node to subscribe to the topics that provide these shards. The DAL node provides the shards and the baker attests that the shards are available. The command returns all of the topics that the baker is subscribed to in the format `{"slot_index":,"pkh":"
"}` where `index` varies between `0` included and the number of slot indexes excluded. :::note DAL nodes share shards and information about them over a peer-to-peer pub/sub network built on the Gossipsub P2P protocol. As layer 1 assigns shards to the bakers, the Gossipsub network manages topics that DAL nodes can subscribe to. For example, if a user submits data to slot 1, the network creates a list of topics: a topic to identify the slot 1 shards assigned to baker A, a topic to identify the slot 1 shards assigned to baker B, and so on for all the bakers that are assigned shards from slot 1. ::: 5. Verify that the baker is attesting DAL data. The easiest way is to check a block explorer that shows information about bakers and their rewards. For example, the block explorer https://tzkt.io shows DAL attestation rewards as separate rewards from ordinary consensus attestation rewards, as in this picture: The TZKT block explorer, showing rewards for DAL attestation If you see that rewards are missed, as in this picture, verify that your baker and DAL node are running and that you are using a DAL companion key: The TZKT block explorer, showing missing rewards for DAL attestation Another way to verify that the baker is attesting DAL data, you can look at the baker logs to see if it injects the expected operations. At each level, the baker is expected to do a subset of these operations: * Receive a block proposal (log message: "received new proposal ... at level ..., round ...") * Inject a preattestation for it (log message: "injected preattestation ... for my_baker (
) for level ..., round ...") * Receive a block (log message: "received new head ... at level ..., round ...") * Inject a consensus attestation for it (log message: "injected attestation ... for my_baker (
) for level ..., round ...") * Attach a DAL attestation to it, indicating which of the shards assigned to the baker have been seen on the DAL network (log message: "ready to attach DAL attestation for level ..., round ..., with bitset ... for my_baker (
) to attest slots published at level ...") ## Backing up and restoring the baker The Octez baking daemon stores persistent operational data in the Octez client's data directory, notably consensus high-water marks and [random seed nonces](https://octez.tezos.com/docs/active/randomness_generation.html). If you want to back up the baker or move it to another machine and restore it, you must copy the nonce file or files from the Octez client's data directory to the equivalent directory on the new machine. These nonce files are named `net_stateful_nonces` and `net_nonces`, where `` is the ID of the network, such as `netXdQprcVkpaWU_stateful_nonces` for Mainnet or `NetXsqzbfFenS_stateful_nonces` for Shadownet. All deployments have the `net_stateful_nonces` file but only legacy baking deployments running versions of Octez prior to 20.0rc1 have the `net_nonces` file. After you have moved the nonce files to the new machine and verified that the baker runs normally for one cycle, you can remove the legacy `net_nonces` file. ## Upgrading the baker and accuser Beginning with Octez version 23.0, upgrading the baker and accuser is just like upgrading the node or any other part of the Octez suite. When a new version of Octez becomes available, you can install the new version of Octez or replace the installed binaries on your system, restart them, and verify that they run correctly. ## Waiting for attestation rights If you are setting up a new baker, you must wait until it receives attestation rights before it can bake blocks or attest to DAL data. The delay to receive attestation rights is a number of cycles determined by the value of the `consensus_rights_delay` constant. You must wait until the end of the current cycle and then for this number of cycles to pass before the baker receives attestation rights. You can check the time remaining in the current cycle on most block explorers. For example, you can go to https://tzkt.io/ and click the network indicator at the top left of the page, as in this picture: The TZKT block explorer, showing information about the current cycle At the end of the current cycle, baking rights for a future cycle are calculated and should include rights for your baker. You can see when your baker will receive attestation rights by checking block explorers after the end of the cycle in which you staked your tez. For example, [TzKT](https://tzkt.io/) has a "Schedule" page that shows baker performance in the recent past and their projected rights in the near future. The following screencap shows the schedule for a new baker. In this case, the baker has attestation rights for two cycles at the times specified in the schedule. The TZKT block explorer Schedules page, showing attestation rights The delay on Mainnet is about 2 days because a cycle is about 1 day and consensus rights are calculated 2 cycles in advance (as set by the `consensus_rights_delay` constant). Block drift and the remaining time in the current cycle can add to this delay. You can calculate the approximate time that it takes a new baker to receive attestation rights with this formula, where `remaining_current_cycle` is the time remaining in the current cycle and the other values are protocol constants: ``` remaining_current_cycle + (consensus_rights_delay * blocks_per_cycle * minimal_block_delay) ``` To get a protocol constant, make sure that your Octez client is connected to the correct network and then call the `/chains/main/blocks/head/context/constants` RPC endpoint. For example, this command gets the `consensus_rights_delay` constant: ```bash octez-client rpc get /chains/main/blocks/head/context/constants | jq .consensus_rights_delay ``` After the delay has passed, **the baker log** (not the Octez node log, neither the DAL node log) should contain lines that look like this: * Consensus pre-attestations: `injected preattestation ...` * Consensus attestations: `injected attestation ...` * Attach DAL attestations: `ready to attach DAL attestation ...` These lines log the attestations that the baker makes. If the baker does not have attestation rights, the log contains lines that start with `The following delegates have no attesting rights at level ...`. :::note Even though the baker binary is using the consensus key, the attestations refer to the main baker key. In spite of this, the baking daemon does not need access to the baker key. You can see the keys that the baker uses in its log. When the baker injects a preattestation with a consensus key, the log shows that it used a consensus key on behalf of the main baker key, as in this example: ```log Aug 22 12:52:40.033 NOTICE β”‚ received new forge event: Aug 22 12:52:40.033 NOTICE β”‚ preattestation ready for delegate consensus_key (tz1gZBk9vtND5ykBmgBkhjPvUfZGD58yvX2R) Aug 22 12:52:40.033 NOTICE β”‚ on behalf of tz1QCVQinE8iVj1H2fckqx6oiM85CNJSK9Sx at level 3961560 (round 0) Aug 22 12:52:40.047 NOTICE β”‚ injected preattestation ooE8gUSRYxnKhQUFuzZf2TtpseGdxCmP54YZq3CEShwUn9vB8Bp Aug 22 12:52:40.047 NOTICE β”‚ for consensus_key (tz1gZBk9vtND5ykBmgBkhjPvUfZGD58yvX2R) Aug 22 12:52:40.047 NOTICE β”‚ on behalf of tz1TGKSrZrBpND3PELJ43nVdyadoeiM1WMzb for level 3961560, round Aug 22 12:52:40.047 NOTICE β”‚ 0 ``` ::: After the attestation delay, whether or not you have attestation rights, proceed to [Step 5: Verifications](/tutorials/join-dal-baker/verify-rights). ## Optional: Run an accuser The accuser is a daemon that monitors blocks and looks for problems, such as bakers who double-sign blocks or inject multiple attestations. If it finds a problem, it posts a denunciation operation, which leads to penalizing the misbehaving baker. You don't have to run an accuser, but if you do, you can receive as a reward part of the penalties of the denounced baker. For example, if your operating system uses the `systemd` software suite, the attester service file might look like this example: ```systemd title="/etc/systemd/system/octez-accuser.service" [Unit] Description=Octez accuser Wants=network-online.target After=network-online.target Requires=octez-node.service [Install] WantedBy=multi-user.target [Service] Type=simple User=tezos ExecStart=octez-accuser run WorkingDirectory=/opt/octez-accuser Restart=on-failure RestartSec=5 StandardOutput=append:/opt/octez-accuser.log StandardError=append:/opt/octez-accuser.log SyslogIdentifier=%n ``` # Step 5: Verifications After the delay that you calculated in [Step 4: Run an Octez baking daemon](/tutorials/join-dal-baker/run-baker), follow these instructions to verify the activity or diagnose and fix issues. 1. Record the address of your baker account (not the consensus account) in an environment variable so you can use it for commands that cannot get addresses by their Octez client aliases: ```bash MY_BAKER="$(octez-client show address my_baker | head -n 1 | cut -d ' ' -f 2)" ``` 2. Run these commands to get the (consensus) attestation rights for the baker in the current cycle: 1. Get the current cycle by running this command: ```bash octez-client rpc get /chains/main/blocks/head | jq | grep '"cycle"' ``` 2. Use the current cycle as the `` parameter in this command. Beware, this command may take several minutes to finish if the list of rights is long: ```bash octez-client rpc get "/chains/main/blocks/head/helpers/attestation_rights?delegate=$MY_BAKER&cycle=" ``` When the baker has attestation rights, the command returns information about them, as in this example: ```json [ { "level": 9484, "delegates": [ { "delegate": "tz1QCVQinE8iVj1H2fckqx6oiM85CNJSK9Sx", "first_slot": 280, "attestation_power": 58, "consensus_key": "tz4RiYrFLkAvwix6GBuEwzKf1zk7XH85qNxu" } ] } ... ] ``` If the command returns an empty array (`[]`), the baker has no rights in the specified cycle. * Check to see if you will receive rights two cycles in the future, using commands similar to those above for the current cycle. You can see who will receive rights no farther than two cycles in the future. This number of cycles is set by the `consensus_rights_delay` network parameter. If this returns a list of future attestation rights for your account, you must wait for that cycle to arrive. * Otherwise, make sure that your node and baker are running. * Verify that the staked balance of your baker account is at least 6,000 tez by running the command `octez-client get staked balance for my_baker`. If the response is less than 6,000 tez, you have not staked enough. Ensure that you are registered as a delegate and stake more tez, retaining a small amount for transaction fees. If necessary you can get more from the faucet. * Check to see if you are active and re-register as a delegate if necessary: 1. Run this command to see if your account is marked as inactive: ```bash octez-client rpc get /chains/main/blocks/head/context/delegates/$MY_BAKER/deactivated ``` Baker accounts are deactivated when the baker is offline for a certain time. 2. If the value for the `deactivated` field is `true`, re-register as a baker by running this command: ```bash octez-client register key my_baker as delegate ``` Note that you do not need to specify the consensus key to reactivate your baker key unless you want to change the consensus key. If you include your current consensus key, the command fails. When the next cycle starts, Tezos calculates attestation rights for two cycles in the future and includes your baker. You can find when the next cycle will start by running these commands: 1. Find the last level of the current cycle by running this command: ```bash octez-client rpc get "/chains/main/blocks/head/helpers/levels_in_current_cycle" ``` ``` 1. Pass the last level of the cycle as the `` parameter in this command: ``` ```` ```bash octez-client rpc get "/chains/main/blocks/head/helpers/attestation_rights?level=" | grep '"estimated_time"' ``` The response shows the estimated time when the cycle will end. ```` You can also find when the next cycle will start by going to a block explorer such as https://shadownet.tzkt.io. For example, this drop-down shows that the next cycle starts in 2 hours: The TZKT block explorer, showing information about the current cycle 3. When your baker receives attestation rights as determined by the `/chains/main/blocks/head/helpers/attestation_rights` RPC call, run this command to get the shards that are assigned to your DAL node for the next block: ```bash octez-client rpc get "/chains/main/blocks/head/context/dal/shards?delegates=$MY_BAKER" ``` The response includes your account's address and a list of shards, as in this example: ```json [ { "delegate": "tz1QCVQinE8iVj1H2fckqx6oiM85CNJSK9Sx", "indexes": [ 25, 27, 67, 73, 158, 494 ] } ] ``` These shards are pieces of data that the baker is assigned to attest. Note that you have to potentially execute the command above during many block levels in order to find a block where you have some shards assigned. There is currently no simple command line to get all your DAL rights for a whole cycle, but you can call it in a loop for future levels until you see some shards. First, get the current level: ```bash octez-client rpc get /chains/main/blocks/head | jq '.header.level' ``` and pass it as the `` parameter in this command: ```bash l=; while true; echo $l; do octez-client rpc get "/chains/main/blocks/head/context/dal/shards?delegates=$MY_BAKER&level=$l"; l=$((l+1)); done ``` If the DAL is active, you should see shards assigned for at least some levels but not necessarily every level. 4. Verify the baker's activity on the Explorus block explorer by going to the Consensus Ops page at https://explorus.io/consensus_ops, selecting Shadownet, and searching for your baker account address (only the first few characters). For example, this screenshot shows consensus operations that include DAL attestations, indicated by a number in the "DAL attestation bitset" column. ![DAL consensus operations, showing DAL consensus operations](/img/tutorials/dal-explorus-consensus-ops.png) If there is no DAL attestation, the block explorer shows a document icon with an X in it: ![](/img/tutorials/dal-explorus-no-attestation-icon.png). This icon can appear before the bakers complete attestations and then turn into a binary number when they attest. If you see the rights, you will see the attestations in the baker's log when scheduled. Now you have a complete DAL baking setup. Your baker is attesting to the availability of DAL data and the DAL node is sharing it to Smart Rollups across the network. If you don't see DAL attestation rights: * Verify that your DAL node is connected to the network by following the instructions in [Troubleshooting](https://octez.tezos.com/docs/shell/dal_run.html#troubleshooting) in the Octez documentation. ## Changing the consensus key or DAL companion key If you need to change the consensus key or DAL companion key that the baker daemon uses, such as if you are not using a `tz4` consensus key and want to take advantage of aggregated attestations as described in [Why set up a consensus key?](/tutorials/join-dal-baker/prepare-account#why-set-up-a-consensus-key) and [Why set up a DAL companion key?](/tutorials/join-dal-baker/prepare-account#why-set-up-a-dal-companion-key), you can change them without changing the baker key. The new keys take effect after the same attestation delay that you had to wait for your baker to receive attestation rights when you first set it up: ``` remaining_current_cycle + (consensus_rights_delay * blocks_per_cycle * minimal_block_delay) ``` Follow these steps to change the keys: 1. Generate a new consensus key or DAL companion key with the `octez-client gen keys -s bls` command or import a private key into the instance of the Octez client on the same machine as the baking daemon. 2. Set the new key with the `octez-client set consensus key for` or `octez-client set companion key for` command. For example, to set the consensus key, run this command, with the address or alias of the new consensus key as the `` placeholder: ```bash octez-client set consensus key for my_baker to ``` Similarly, to set the DAL companion key, run this command, with the address or alias of the new DAL companion key as the `` placeholder: ```bash octez-client set companion key for my_baker to ``` 3. Wait for the change to take effect. During this time you can leave the baking daemon running with the old consensus key. 4. When the new key is active, stop the baking daemon and restart it with the new key. To revoke the consensus key, set the consensus key to the baker key, as in this command: ```bash octez-client set consensus key for my_baker to my_baker ``` You cannot revoke the DAL companion key; you can only change it to a new DAL companion key. Consensus keys can transfer the liquid (unstaked) tez from the baker key to any other account with the `drain delegate` command, as in this example: ```bash octez-client drain delegate my_baker to consensus_key with consensus_key ``` The TzKt block explorer shows a baker's consensus key and DAL companion key, along with pending changes to them, on the **Secondary keys** tab: The TZKT block explorer, showing information about a baker's current keys and a pending change to its DAL companion key ## Unstaking your tez and receiving your baking rewards If you leave the baker running, you can see rewards accrue by running the command `octez-client get staked balance for my_baker`. This amount starts at the amount that you originally staked and increases with your baking rewards. You can unstake your tez and withdraw your stake and any baking rewards with the `octez-client unstake` command. For example, this command unstakes 6,000 tez: ```bash octez-client unstake 6000 for my_baker ``` You can substitute "everything" for the amount of tez to unstake everything. Then, after the same delay of `consensus_rights_delay` cycles, an automated system finalizes the unstake request. If this system misses your unstake request or is not running, you can finalize the unstake request yourself by running this command: ```bash octez-client finalize unstake for my_baker ``` Then you can do whatever you want with the tez, including sending it back to the faucet for someone else to use. The Shadownet faucet's address is `tz1a4GT7THHaGDiTxgXoatDWcZfJ5j29z5RC`, so you can send funds back with this command: ```bash octez-client transfer 6000 from my_baker to tz1a4GT7THHaGDiTxgXoatDWcZfJ5j29z5RC ``` For a summary, see [Conclusion](/tutorials/join-dal-baker/conclusion). # Conclusion In this guide you have gone through all the steps needed to participate as a baker and DAL node. The steps for participating on any other network, including Tezos Mainnet, are similar, but other networks have different parameters. For example, the attestation delay on Mainnet is 2 weeks. You could further improve the setup by defining system services so that the daemons are automatically launched when the machine starts. You could also plug a monitoring solution such as the Prometheus + Grafana combo; a Grafana dashboard template for DAL nodes is available in Grafazos. The interactions between your baker and the chain can be observed on the Explorus block explorer which is aware of the DAL and can in particular display which DAL slots are being used at each level. # Bake using a Ledger device Estimated time: 1 hour in addition to time setting up the the baker as covered in \[Run a Tezos baker in 5 steps]0(/tutorials/join-dal-baker) ## What is a Ledger device? A Ledger device is a physical wallet provided by [Ledger](https://www.ledger.com). Its main purpose is to store the holder's private keys without ever disclosing them. Ledger devices support many blockchains by installing applications, such as an application to manage Tezos accounts and keys and an application to allow a Tezos baker to use keys on the Ledger. ## Why use a Ledger device to bake? The baker daemon must have constant access to the baker's private key so that it can sign consensus operations and blocks. If a malicious entity manages to get access to this private key, it will also gain access to the baker's funds. Keeping your private key on a Ledger device and only interacting with an application dedicated to baking would prevent any direct access to your private key. ## Setting up your Ledger to launch a baker signing with Ledger Follow this tutorial before setting up your baker with the tutorial [Run a Tezos baker in 5 steps](/tutorials/join-dal-baker). This tutorial will tell you when to switch to that tutorial and what changes to make so the baker you set up will use the accounts on your Ledger device. In this tutorial, we'll look at: * how to install the Tezos baking application on your Ledger device * how to configure your Ledger device so that the [Ledger baking application of Tezos](https://github.com/trilitech/ledger-app-tezos-baking) works properly * how to use an external signer (`octez-signer`) while running your baker for enhanced protection ## Prerequisites * A Ledger device: Nano S, Nano S+, Nano X, Stax or Flex * A computer or cloud VM that can run without interruptions, because the baker program must run persistently * The latest version of the Octez suite, including the `octez-signer` program :::note Note that a PIN input will be required after a power failure. To ensure a truly persistent system, please use a [UPS](https://wikipedia.org/wiki/Uninterruptible_power_supply). ::: # Install the Ledger baking application of Tezos [`Tezos Baking`](https://github.com/trilitech/ledger-app-tezos-baking) is the application developed to bake on Tezos using your Ledger device. It allows you to sign block and consensus operations while keeping your private keys secure in the Ledger hardware. Some of its additional features are: 1. HWM tracking to avoid double baking 2. Restricted signing permission, i.e. it only allows signing baking related operations. You can not approve signing of funds transfer using baking app on Ledger. ## Download `Ledger Live` To download the Tezos baking application, you first need to download `Ledger Live`. [`Ledger Live`](https://www.ledger.com/ledger-live) is the application provided by Ledger to allow you to download the various applications compatible with your Ledger device. ## Download `Tezos Baking` Once you have downloaded `Ledger Live`, launch it. The Tezos baking application is only available when developer mode is activated. To activate it, go to settings and, in the `Experimental features` tab, activate `Developer mode`. With developer mode enabled, the Tezos baking application is now accessible. Click on `My Ledger`. If you have not already done so, connect your Ledger device to the USB port and authorize the secure connection to `Ledger Live` on your Ledger device. Search for the `Tezos Baking` application and click on `Install`. ![Install the Ledger Tezos Baking application from Ledger Live](/img/tutorials/bake-with-ledger/install-ledger-tezos-baking-app.gif) ## Download `Tezos Wallet (XTZ)` To be able to sign the operations needed to set up your baker, you also need the `Tezos Wallet (XTZ)` application. [`Tezos Wallet (XTZ)`](https://github.com/trilitech/ledger-app-tezos-wallet) is the application developed to sign Tezos operations using your Ledger device. Find and install the `Tezos Wallet (XTZ)` application. # Set up your ledger ## Disable PIN lock The Tezos baking application allows you to bake securely without interruption. However, you will need to disable auto PIN lock feature in the Ledger to avoid getting locked out of the Ledger. Otherwise the Ledger device will lock itself and baking app will not work. :::note Warning Disabling the automatic lock on your Ledger device poses a risk that if any other app except baking-app is left open on your device, someone could get access to your funds by using that Ledger if left unattended. The Tezos baking application is extremely secure and it only allows you to sign baking-related transactions and requires a PIN code to exit the application. However, remember to **reactivate the automatic lock on your Ledger device if you stop using the Tezos baking application on this device**. ::: Go to the settings of your Ledger device and search for the automatic PIN lock option, then deactivate it. * For **NanoS, NanoS+ and NanoX** devices: Go to `Settings` > `Security` > `PIN lock`, then select `No PIN lock` (`Off` for **NanoS**). * For **Stax and Flex** devices: Go to `Settings` > `Lock screen` > `Auto-lock`, then disable `Auto-lock`. ## Charging & Battery Saver Considerations Since your baker runs continuously, it is **strongly recommended to keep your Ledger device constantly powered** to prevent it from running out of battery. On **NanoX, Stax, and Flex** devices, a battery saver setting allows your Ledger to automatically power off after a period of inactivity to preserve battery life. However, since the Baking app requires the device to remain active at all times, it is **highly recommended to disable this option**. * For **NanoX** devices: Go to `Settings` > `General` > `Battery Saver`, then select `Never power off`. * For **Stax and Flex** devices: Go to `Settings` > `Battery` > `Auto Power-Off`, then disable `Auto Power-Off`. ## Screen saver In order to preserve the performance and integrity of your Ledger device, it is **strongly recommended** to activate the screen saver of your Ledger device. Go to the settings of your Ledger device and look for the screen saver option, then activate it for a value that suits you. * For **NanoS, NanoS+ and NanoX** devices: Go to `Settings` > `Security` > `Screen saver`. * For **Stax and Flex** devices there is no screen saver as of writing this article (Jan 25). ## HWM option :::note Warning HWM (High Watermark) protection exists in the Ledger `Tezos Baking` application to avoid double-baking, double-attesting or double-preattesting at the level. The HWM is stored in NVRAM (Non-volatile Random Access Memory), after every signature, by the `Tezos Baking` application (that is on each pre-attestation, attestation, but also while signing blocks). The NVRAM on Ledger has limited read/write lifetime, thus frequent updates of NVRAM leads to NVRAM burn. To resolve this, an **optional** setting called HWM (ENABLE/DISABLE) is added to the Ledger `Tezos Baking` application (since v 2.4.7). When disabled, it allows storing HWM on RAM instead of NVRAM during the signature of operations. This increases the speed/performance of the Ledger `Tezos Baking` application and extends the lifetime of Ledger devices. The last HWM value on the Ledger’s RAM is written to NVRAM at the time of exiting the Ledger `Tezos Baking` application for persistent storage. In case of an abrupt interruption of the Ledger `Tezos Baking` application, e.g. caused by an abrupt power off of the Ledger device, the current HWM value may not be updated to the device’s NVRAM. Thus, it’s important to reset the value of the HWM on the Ledger device to the highest HWM value signed by the baker, before resuming baking. (See [Setup the Ledger high watermark (HWM)](/tutorials/bake-with-ledger/run-baker#setup-the-ledger-high-watermark-hwm) to setup the HWM) ::: For additional protection from double-baking, this tutorial demonstrates the use of an external signer (`octez-signer`), which keeps track of HWM and prevents double baking. It's recommended to use this external signer when you disable the HWM feature on your Ledger device. # Set up your Ledger baker key with octez-signer It’s recommended to use a separate machine to run the remote signer. For simplicity, in this tutorial, we assume a setup where the Ledger device is connected to the same machine running the baker binary. On the same machine, the following commands can be used to set up the baker key with `octez-signer`. ## Import a key from your Ledger device to the `octez-signer` context Let's start by importing a key from your Ledger device for `octez-signer`. Connect your Ledger device with a USB cable and open the `Tezos Baking` application. To see the available keys, run: ```bash octez-signer list connected ledgers ``` Output: ```console ## Ledger `masculine-pig-stupendous-dugong` Found a Tezos Baking 2.4.7 (git-description: "v2.4.7-70-g3195b4d2") application running on Ledger Nano S Plus at [1-1.4.6:1.0]. To use keys at BIP32 path m/44'/1729'/0'/0' (default Tezos key path), use one of: octez-client import secret key ledger_username "ledger://masculine-pig-stupendous-dugong/ed25519/0h/0h" octez-client import secret key ledger_username "ledger://masculine-pig-stupendous-dugong/secp256k1/0h/0h" octez-client import secret key ledger_username "ledger://masculine-pig-stupendous-dugong/P-256/0h/0h" octez-client import secret key ledger_username "ledger://masculine-pig-stupendous-dugong/bip25519/0h/0h" ``` Key's URIs are of the form `ledger:///[/]` where: * `` is the identifier of the ledger. * `` is the signing curve * `` is a BIP32 path anchored at m/44h/1729h. The ledger does not yet support non-hardened paths, so each node of the path must be hardened. :::note Signing curve The `secp256k1` and `P-256` signature schemes (resp. `tz2` and `tz3`) have the best signature performance with the `Tezos Baking` application. ::: Choose one of the URIs shown, modifying the BIP32 path as you like, then import it using `octez-signer`: ```bash octez-signer import secret key my_ledger_key "ledger://masculine-pig-stupendous-dugong/secp256k1/0h/0h" ``` On your Ledger device, you should see a screen sequence similar to: ![Ledger Key Review](/img/tutorials/bake-with-ledger/pkh-review.png) If the public key hash displayed on your Ledger is equal to the address displayed in the command output, you can approve. Output: ```console Please validate (and write down) the public key hash displayed on the Ledger, it should be equal to `tz...`: Tezos address added: tz... ``` ## Authorise the baker key in the `Tezos Baking` application For your security, the `Tezos Baking` application only allows one key to be used for signing. So you need to specify which key you want to bake with: ```bash octez-signer setup ledger to bake for my_ledger_key ``` On your Ledger device, you should see a screen sequence similar to: ![Ledger Setup Review](/img/tutorials/bake-with-ledger/setup-review.png) If the information displayed on your Ledger is similar to the information displayed in the command output, you can approve. Output: ```console Setting up the ledger: * Main chain ID: 'Unspecified' -> NetXdQprcVkpaWU * Main chain High Watermark: 0 (round: 0) -> 0 (round: 0) * Test chain High Watermark: 0 (round: 0) -> 0 (round: 0) Authorized baking for address: tz... Corresponding full public key: ..pk... ``` ## Link `octez-signer` to `octez-client` Now that your baker key on `octez-signer` is linked to your Ledger device, `octez-signer` will be in charge of signing using your Ledger device. Let's launch `octez-signer`: ```bash octez-signer launch socket signer -a localhost ``` > The default port is `7732`. To be able to sign from `octez-client` and from the baker binaries, you have to link your remote signer for `octez-client`. In a new terminal, run: ```bash octez-client -R 'tcp://localhost:7732' config update ``` This way, the key stored in the context of your `octez-signer` will be accessible by remote from the `octez-client` context. # Running a baker signing using a Ledger baker key Now that the Ledger baker key is set up, you can follow the steps of [Run a Tezos baker in 5 steps](/tutorials/join-dal-baker). However, some steps will differ. ## Set up a baker account Complete the [Step 1: Run an Octez node](/tutorials/join-dal-baker/run-node) of the tutorial, and make following changes in [Step 2: Set up a baker account](/tutorials/join-dal-baker/prepare-account). You can use your Ledger key as your main baker key or you could use Ledger key as consensus key. * To **use the Ledger key as your main baker key**, import it from the `octez-signer` remote with the following command: ```bash octez-client import secret key my_baker remote:tz... ``` > Replace the `tz...` with the public key hash of your Ledger baker key. Run and sign the following operations to set up your baker. You will need to use the `Tezos Wallet (XTZ)` application. Quit the `Tezos Baking` application and open the `Tezos Wallet (XTZ)` application. Then set up your baker. ```bash octez-client import secret key my_baker remote:tz... octez-client register key my_baker as delegate octez-client stake 6000 for my_baker ``` Your baker account is now set up and ready to bake using the Ledger. * If you **want to use your Ledger key as a consensus key**, import it from the `octez-signer` remote with the following command: ```bash octez-client import secret key consensus_key remote:tz... ``` > Replace the `tz...` with the public key hash of your Ledger baker key. With Ledger key imported as consensus key, you will need to generate/set up your baker key separately. You can then continue to set up your baker account. See the following commands: ```bash octez-client gen keys my_baker octez-client register key my_baker as delegate with consensus key consensus_key octez-client stake 6000 for my_baker ``` By registering your baker as a delegate with the ledger key as the consensus key, the baker daemon will sign using the Ledger. ## Before running the Octez baking daemon Complete [Step 3: Run an Octez DAL node](/tutorials/join-dal-baker/run-dal-node). For the [Step 4: Run an Octez baking daemon](/tutorials/join-dal-baker/run-baker), make following changes to setup `octez-signer` and `Tezos Baking` application. ### Setup the Ledger high watermark (HWM) For security reasons, always reset HWM to the highest possible block value before starting to bake. The highest block can be obtained from [Tzkt](https://www.tzkt.io/blocks?expand=1). Then, use that block value as the level in the following command. Go back to the `Tezos Baking` application and run: ```bash octez-signer set ledger high watermark for my_ledger_key to ``` On your Ledger device, you should see a screen sequence similar to: ![Ledger Setup Review](/img/tutorials/bake-with-ledger/set-hwm-review.png) Check that the HWM is the one you supplied, then you can approve. Output: ```console ledger://masculine-pig-stupendous-dugong/secp256k1/0h/0h has now high water mark: 42 (round: 0) ``` :::note Alternatively, the HWM can be set up from the setup command: ```bash octez-signer setup ledger to bake for my_ledger_key --main-hwm ``` ::: ### Set up additional checks for `octez-signer` `octez-signer` also has the ability to enable various checks. Stop the previously launched `octez-signer` TCP socket and restart it with the following command: ```bash octez-signer launch socket signer -M 0x11,0x12,0x13 -W -a localhost ``` > The `-M 0x11,0x12,0x13` option is used to only request consensus operations and blocks to be signed. > The `-W` tag is used to activate the HWM check. :::note Warning The `-W` tag is required if you have chosen to disable the `High Watermark` option in the `Tezos Baking` application. ::: ## Security verifications Everything is ready, you can now finish the tutorial [Run a Tezos baker in 5 steps](/tutorials/join-dal-baker). The baking daemon will send the data to be signed to `octez-signer` which will send it to your Ledger device, which will sign them. Once the baking daemon has started, you can check on your Ledger device that the HWM is evolving in accordance with the blocks signed by your Ledger baker key. The `octez-signer` also stores the HWM for the blocks it has signed. You can find them in a file named `Net..._highwatermarks` in the `.tezos-client` folder. > `Net...` being the chain-id of the chain in which you bake. Open the file and check that the HWMs evolve in accordance with the blocks signed by your Ledger baker key: ```bash cat .tezos-client/NetXnHfVqm9iesp_highwatermarks ``` ```json { "blocks": [ { "delegate": "tz...", "highwatermark": { "round": 0, "level": 107095 } } ], "preattestations": [ { "delegate": "tz...", "highwatermark": { "round": 0, "level": 107096 } } ], "attestations": [ { "delegate": "tz...", "highwatermark": { "round": 0, "level": 107096 } } ] } ``` Now the baking daemon is running and using the Ledger to sign consensus (baking) operations. You can leave the baker running and check on it by looking at the block numbers at the end of the `.tezos-client/NetXnHfVqm9iesp_highwatermarks` file. # Deploy a Smart Rollup Estimated time: 90 minutes This tutorial covers how to deploy a Smart Rollup in a Tezos sandbox. To run this tutorial, you should have a basic understanding of how Tezos works and the ability to use the command-line terminal on your computer. In this tutorial, you will learn: * What a Smart Rollup is and how they help scale Tezos * How information passes between Tezos and Smart Rollups * How to respond to messages from Tezos in a Smart Rollup ## What is a Smart Rollup? Smart Rollups are processing units that run outside the Tezos network but communicate with Tezos on a regular basis. These processing units can run arbitrarily large amounts of code without waiting for Tezos baking nodes to run and verify that code. Smart Rollups use Tezos for information and transactions but can run large applications at their own speed, independently of the Tezos baking system. In this way, Smart Rollups allow Tezos to scale to support large, complex applications without slowing Tezos itself or incurring large transaction and storage fees. The processing that runs on Tezos itself via smart contracts is referred to as *layer 1* and the processing that Smart Rollups run is referred to as *layer 2*. To learn about running code in smart contracts, see the tutorial [Deploy a smart contract](/tutorials/smart-contract). Rollups also have an outbox, which consists of calls to smart contracts on layer 1. These calls are how rollups send messages back to layer 1. Smart Rollups can run any kind of applications that they want, such as: * Financial applications that use information and transactions from Tezos * Gaming applications that manipulate assets and keep them in sync with Tezos * Applications that run complex logic on NFTs or other types of tokens * Applications that communicate with other blockchains Smart Rollups power expansions of the Tezos ecosystem, including [Etherlink](https://www.etherlink.com/). Smart Rollups maintain consensus by publishing the hash of their state to Tezos, which other nodes can use to verify the rollup's behavior. The specific way that rollups publish their states and maintain consensus is beyond the scope of this tutorial. For more information about rollups and their consensus mechanism, see [Smart Optimistic Rollups](/architecture/smart-rollups). This diagram shows a Smart Rollup interacting with layer 1 by receiving a message, running processing based on that message, and sending a transaction to layer 1: ![Diagram that shows the flow of messages in Smart Rollups](/img/tutorials/smart-rollup-overview.png) Smart Rollups stay in sync with Tezos by passing messages to Tezos and receiving messages from Tezos and other rollups. Each Tezos block contains a global rollups inbox that contains messages from Tezos layer 1 to all rollups. Anyone can add a message to this inbox and all messages are visible to all rollups. Rollups receive this inbox, filter it to the messages that they are interested in, and act on them accordingly. ## Smart Rollup analogy Businesses talk about *horizontal scaling* versus *vertical scaling*. If a business is growing and its employees are being overworked, the business could use vertical scaling to hire more employees or use better tools to improve the productivity of each employee. Scaling Tezos in this way would mean using more processing power to process each new block, which would increase the cost to run baking nodes. Also, if the business hires more employees, the amount of communication between employees increases because, for example, they have to make sure that they are working in the same way and not doing duplicate jobs. By contrast, Smart Rollups behave like horizontal scaling. In horizontal scaling, businesses create specialized teams that work on different portions of the workload. These teams can work independently of other teams and take advantage of efficiencies of being focused on a specific task. They also need to communicate less with other teams, which speeds up their work. Smart Rollups are like separate horizontally scaled teams, with Tezos layer 1 as the source of communication between teams. ## Prerequisites To run this tutorial, make sure that the following tools are installed: * [Docker](https://www.docker.com/) * Rust The application in this tutorial uses Rust because of its support for WebAssembly (WASM), the language that Smart Rollups use to communicate. Rollups can use any language that has WASM compilation support. To install Rust via the `rustup` command, run this command: ```bash curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` You can see other ways of installing Rust at https://www.rust-lang.org. * Clang and LLVM Clang and LLVM are required for compilation to WebAssembly. Version 11 or later of Clang is required. Here are instructions for installing the appropriate tools on different operating systems: **MacOS** ```bash brew install llvm export CC="$(brew --prefix llvm)/bin/clang" ``` In some cases for MacOS you may need to update your `PATH` environment variable to include LLVM by running this command: ```bash echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.zshrc ``` **Ubuntu** ```bash sudo apt-get install clang-11 export CC=clang-11 ``` **Fedora** ```bash dnf install clang export CC=clang ``` **Arch Linux** ```bash pacman -S clang export CC=clang ``` The `export CC` command sets Clang as the default C/C++ compiler. After you run these commands, run `$CC --version` to verify that you have version 11 or greater installed. Also, ensure that your version of Clang `wasm32` target with by running the command `$CC -print-targets | grep wasm32` and verifying that the results include `wasm32`. * AR (macOS only) To compile to WebAssembly on macOS, you need to use the LLVM archiver. If you used Homebrew to install LLVM, you can configure it to use the archiver by running this command: ```bash export AR="$(brew --prefix llvm)/bin/llvm-ar" ``` * WebAssembly Toolkit The [WebAssembly Toolkit (`wabt`)](https://github.com/WebAssembly/wabt) provides tooling for reducing (or *stripping*) the size of WebAssembly binaries (with the `wasm-strip` command) and conversion utilities between the textual and binary representations of WebAssembly (including the `wat2wasm` and `wasm2wat` commands). Most distributions ship a `wabt` package, which you can install with the appropriate command for your operating system: **MacOS** ```bash brew install wabt ``` **Ubuntu** ```bash sudo apt install wabt ``` **Fedora** ```bash dnf install wabt ``` **Arch Linux** ```bash pacman -S wabt ``` To verify that `wabt` is installed, run the command `wasm-strip --version` and verify that the version is at least 1.0.31. If not, you can download this version directly and extract its files: https://github.com/WebAssembly/wabt/releases/tag/1.0.31. Then, whenever you have to use `wasm-strip`, you can use `/bin/wasm-strip` instead. ## Tutorial application Despite the number of command-line tools needed, the code for the core of the rollup itself is relatively simple. This core is called the *kernel* and is responsible for accepting messages from layer 1 and sending messages to layer 1. The code for the tutorial application is here: https://gitlab.com/trili/hello-world-kernel. The code for the kernel is in the `src/lib.rs` file. It is written in the Rust programming language and looks like this: ```rust use tezos_smart_rollup::inbox::InboxMessage; use tezos_smart_rollup::kernel_entry; use tezos_smart_rollup::michelson::MichelsonBytes; use tezos_smart_rollup::prelude::*; kernel_entry!(hello_kernel); fn handle_message(host: &mut impl Runtime, msg: impl AsRef<[u8]>) { if let Some((_, msg)) = InboxMessage::::parse(msg.as_ref()).ok() { debug_msg!(host, "Got message: {:?}\n", msg); } } pub fn hello_kernel(host: &mut impl Runtime) { debug_msg!(host, "Hello, kernel!\n"); while let Some(msg) = host.read_input().unwrap() { handle_message(host, msg); } } ``` This example kernel has these major parts: 1. It imports resources that allow it to access and decode messages from layer 1. 2. It runs the Rust macro `kernel_entry!` to set the main function for the kernel. 3. It declares the `handle_message` function, which accepts, decodes, and processes messages from layer 1. In this case, the function decodes the message (which is sent as a sequence of bytes) and prints it to the log. The function could call any other logic that the application needs to run. 4. It declares the `hello_kernel` function, which is the main function for the kernel. It runs each time the kernel receives messages from layer 1, prints a logging message each time it is called, and runs the `handle_message` function on each message. You don't need to access the other files in the application directly, but here are descriptions of them: * `src/lib.rs`: The Rust code for the kernel * `Cargo.toml`: The dependencies for the build process * `rustup-toolchain.toml`: The required Rust version * `sandbox_node.sh`: A script that sets up a Tezos sandbox for testing the rollup The tutorial repository also includes two files that represent example message inboxes in layer 1 blocks: * `empty_input.json`: An empty rollup message inbox * `two_inputs.json`: A rollup message inbox with two messages When you're ready, move to [Part 1: Running the kernel in debug mode](/tutorials/smart-rollup/debug). # Part 1: Running the kernel in debug mode To set up the application for the tutorial, you must configure Rust to build the kernel. Then you can run the kernel in debug mode to see how it works. Before you begin, make sure that you have installed the prerequisites in [Deploy a Smart Rollup](/tutorials/smart-rollup#prerequisites). ## Downloading and building the kernel Follow these steps to get the kernel code and build it: 1. Clone the repository with the kernel code and go into its directory: ```bash git clone https://gitlab.com/trili/hello-world-kernel.git cd hello-world-kernel/ ``` 2. Configure Rust to build WebAssembly applications: 1. Verify that you have Rust version 1.73.0 or later installed by running `rustc --version`. 2. If you have a version of Rust later than 1.73.0, use the `rustup override` command to use version 1.73.0: ```bash rustup override set 1.73.0 ``` 3. Add WASM as a compilation target for Rust by running this command: ```bash rustup target add wasm32-unknown-unknown ``` 3. Build the kernel by running this command: ```bash cargo build --target wasm32-unknown-unknown ``` If the kernel builds correctly, the terminal shows a message that looks like "Finished dev \[unoptimized + debuginfo] target(s) in 15s." The compiled kernel files are in the `target/wasm32-unknown-unknown/debug` folder. In particular, the compiled kernel itself is in the `hello_world_kernel.wasm` file. Now the kernel is compiled into a single file that nodes can run. ## Debugging the kernel Octez provides an executable named `octez-smart-rollup-wasm-debugger` that runs Smart Rollups in debug mode to make it easier to test and observe them. Later, you will deploy the rollup to the sandbox, but running it in debug mode first verifies that it built correctly. This executable is not provided in Octez releases or Docker containers, so you must set up the Octez source code and run it from there. 1. Install the opam package manager as described in its documentation: https://opam.ocaml.org/doc/Install.html. 2. In a directory outside the `hello-world-kernel` directory, clone the Octez repository by running this command: ```bash git clone https://gitlab.com/tezos/tezos.git ``` 3. Go into the `tezos` directory: ```bash cd tezos ``` 4. Run this command to install the necessary dependencies: ```bash make build-dev-deps ``` 5. From the `tezos` directory, run this command to start the Smart Rollup in debug mode and pass an empty message inbox to it, changing `` to the path to the `hello-world-kernel` directory: ```bash dune exec src/bin_wasm_debugger/main_wasm_debugger.exe -- \ --kernel /target/wasm32-unknown-unknown/debug/hello_world_kernel.wasm \ --inputs /empty_input.json ``` The command prompt changes to show that you are in debugging mode, which steps through commands. If you see an error, make sure that the kernel built properly and that the paths in the command are correct. 6. At the debugging prompt, run this command to send the message inbox to the kernel: ```bash step inbox ``` The response shows the logging information for the kernel, including these parts: * The message "Hello, kernel" from the `hello_kernel` function * The message "Got message: Internal(StartOfLevel)," which represents the start of the message inbox * The message "Got message: Internal(InfoPerLevel(InfoPerLevel ...," which provides the hash and timestamp of the previous block * The message "Got message: Internal(EndOfLevel)," which represents the end of the message inbox 7. Press Ctrl + C to end debugging mode. Now you know that the kernel works. In the next section, you optimize the kernel to be small enough to be deployed. Continue to [Part 2: Optimizing the kernel](/tutorials/smart-rollup/optimize). # Part 2: Optimizing the kernel To originate the kernel on Tezos, it must fit within the maximum size for a layer 1 operation (32KB). In these steps, you reduce the size of the kernel: 1. From the `hello-world-kernel` directory, run this command to print the current size of the kernel: ```bash du -h target/wasm32-unknown-unknown/debug/hello_world_kernel.wasm ``` Because you ran it in debug mode, the size of the compiled kernel and its dependencies may be 18MB or more, which is too large to originate. 2. Create a release build of the kernel: ```bash cargo build --release --target wasm32-unknown-unknown ``` The release build of the kernel is in the `target/wasm32-unknown-unknown/release/` directory. 3. Check the size of the release build of the kernel: ```bash du -h target/wasm32-unknown-unknown/release/hello_world_kernel.wasm ``` The release build is significantly smaller, but still too large. 4. Run the `wasm-strip` command to reduce the size of the kernel: ```bash wasm-strip target/wasm32-unknown-unknown/release/hello_world_kernel.wasm ``` This command removes WebAssembly code that is not necessary to run Smart Rollups. 5. Run the `du` command again to see the new size of the kernel: ```bash du -h target/wasm32-unknown-unknown/release/hello_world_kernel.wasm ``` The size of the kernel is smaller now. To get the kernel running with an even smaller size, you can use the installer kernel, which includes only enough information to install your original kernel. To do this, your kernel is split up and stored in separate files called preimages. When the installer kernel runs, it requests these files and reconstructs the original kernel. 6. Install the installer kernel tool: ```bash cargo install tezos-smart-rollup-installer ``` 7. Run this command to create an installer kernel: ```bash smart-rollup-installer get-reveal-installer \ --upgrade-to target/wasm32-unknown-unknown/release/hello_world_kernel.wasm \ --output hello_world_kernel_installer.wasm --preimages-dir preimages/ ``` This command creates the following files: * `hello_world_kernel_installer.wasm`: The installer kernel * `preimages/`: A directory that contains the preimages that allow nodes to restore the original kernel code When a node runs the installer kernel, it retrieves the preimages through the reveal data channel, a channel that Smart Rollups use to request data from outside of layer 1. For more information about the reveal data channel, see [Reveal data channel](https://octez.tezos.com/docs/active/smart_rollups.html#reveal-data-channel). 8. Verify the size of the installer kernel by running this command: ```bash du -h hello_world_kernel_installer.wasm ``` Now the kernel is small enough to originate on layer 1. 9. As you did in the previous section, from the root of the Octez repository, run the installer kernel in debug mode by running this command, changing `` to the path to the `hello-world-kernel` directory: ```bash dune exec src/bin_wasm_debugger/main_wasm_debugger.exe -- \ --kernel /hello_world_kernel_installer.wasm \ --preimage-dir /preimages \ --inputs /empty_input.json ``` Note that this command uses the installer kernel and provides the location of the preimages in the `--preimage-dir` argument. Then you can use the `step inbox` command to simulate receiving the inbox from layer 1, as you did in the previous section. You can see the hello world kernel messages in the log, which shows that the upgrade from the installer kernel to the full kernel was successful. 10. Press Ctrl + C to end the debugging session. 11. From the `hello-world-kernel` directory, create a hexadecimal version of the installer kernel by running this command: ```bash smart-rollup-installer get-reveal-installer \ --upgrade-to target/wasm32-unknown-unknown/release/hello_world_kernel.wasm \ --output hello_world_kernel_installer.hex --preimages-dir preimages/ ``` In the next section, you set up a sandbox environment to deploy this kernel. Continue to [Part 3: Deploying (originating) to a sandbox](/tutorials/smart-rollup/sandbox). # Part 3: Deploying (originating) to a sandbox Tezos provides a Docker image that contains the Octez client, which allows you to interact with Tezos from the command line. In this section, you use this image to run a sandbox Tezos environment to deploy the Smart Rollup to. ## Setting up the sandbox The `hello-world-kernel` repository includes a script that configures the Octez sandbox environment for the Smart Rollup, including importing bootstrap keys and setting up bakers. It's better to run this script in a Docker container to avoid interfering with your local environment. 1. Make sure that Docker desktop is running. 2. Pull the most recent Tezos Docker image, which contains the most recent version of Octez: ```bash docker pull tezos/tezos:latest ``` 3. Make sure that you are in the `hello-world-kernel` folder, at the same level as the `Cargo.toml` and `sandbox_node.sh` files. 4. Run this command to start the Docker image, open a command-line terminal in that image, and mount the `hello-world-kernel` folder in it: ```bash docker run -it --rm --volume $(pwd):/home/tezos/hello-world-kernel --entrypoint /bin/sh --name octez-container tezos/tezos:latest ``` Your command-line prompt changes to indicate that it is now inside the running Docker container. This image includes the Octez command-line client and other Tezos tools. It also uses the docker `--volume` argument to mount the contents of the `hello-world-kernel` folder in the container so you can use those files within the container. 5. Verify that the container has the necessary tools by running these commands: ```bash octez-node --version octez-smart-rollup-node --version octez-client --version ``` Each of these commands should print a version number. The specific version number is not important as long as you retrieved the latest image with the `docker pull tezos/tezos:master` command. Don't close this terminal window or exit the Docker terminal session, because Docker will close the container. If you accidentally close the container, you can run the `docker run ...` command again to open a new one. Now the application is built and you have an environment that you can debug it in. For the rest of the tutorial, you must be aware of whether you are running commands inside or outside of the Docker container. The container has Octez but not Rust, so you run Rust commands outside of the container and Octez commands inside the container. ## Deploying the Smart Rollup to the sandbox Deploying (originating) a Smart Rollup is similar to deploying smart contracts. Instead of running the `octez-client originate contract` command, you run the `octez-client originate smart rollup` command. This command creates an address for the Smart Rollup and stores a small amount of data about it on layer 1. 1. In the Docker container, in the `hello-world-kernel` folder, run this command to start the sandbox: ```bash cd hello-world-kernel ./sandbox_node.sh ``` This command starts a Tezos testing environment, including a baking node running in sandbox mode and a group of test accounts. The console shows repeated messages that show that the node is baking blocks. For more information about sandbox mode, see [Sandboxed mode](https://octez.tezos.com/docs/user/sandbox.html). If you see an error that says "Unable to connect to the node," you can ignore it because it happens only once while the node is starting. 2. Leave that terminal instance running for the rest of the tutorial. 3. Open a new terminal window. 4. In the new terminal window, enter the Docker container by running this command: ```bash docker exec -it octez-container /bin/sh ``` Now the second terminal window is running inside the container just like the first one. 5. In the new terminal window, go to the folder with the Smart Rollup code: ```bash cd hello-world-kernel ``` 6. In the second terminal window, run this command to verify that the sandbox is running with the correct protocol: ```bash octez-client rpc get /chains/main/blocks/head/metadata | grep protocol ``` The response shows the protocol that the sandbox is running with the alpha protocol, which is the current development version of the Tezos protocol. The response looks like this example: ``` { "protocol": "ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK", "next_protocol": "ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK", ``` If you don't see a message that looks like this one, check for errors in the first terminal window. Now the sandbox is running in the Docker container and you can use it to test the rollup. 7. Run this command to deploy the installer kernel to the Tezos sandbox: ```bash octez-client originate smart rollup \ "test_smart_rollup" from "bootstrap1" \ of kind wasm_2_0_0 of type bytes \ with kernel file:hello_world_kernel_installer.hex --burn-cap 3 ``` If you need to open a new terminal window within the Docker container, run the command `docker exec -it octez-container /bin/sh`. Like the command to originate a smart contract, this command uses the `--burn-cap` argument to allow the transaction to take fees from the account. Also like deploying a smart contract, the response in the terminal shows information about the transaction and the address of the originated Smart Rollup, which starts with `sr1`. Now layer 1 is aware of the Smart Rollup and nodes can run the kernel. Continue to [Part 4: Running and interacting with the Smart Rollup node](/tutorials/smart-rollup/run) to start a Smart Rollup node and interact with the kernel. # Part 4: Running and interacting with the Smart Rollup node Now that the Smart Rollup is originated on layer 1 in the sandbox, anyone can run a Smart Rollup node for it. Smart Rollup nodes are similar to baking nodes, but they run the Smart Rollup kernel instead of baking Tezos blocks. In these steps, you start a Smart Rollup node, but note that anyone can run a node based on your kernel, including people who want to verify the its behavior. ## Running a Smart Rollup node 1. In the Docker container, in the `hello-world-kernel` directory, copy the contents of the `preimages` folder to a folder that the rollup node can access by running these commands: ```bash mkdir -p ~/.tezos-rollup-node/wasm_2_0_0 cp preimages/* ~/.tezos-rollup-node/wasm_2_0_0/ ``` 2. Still in the Docker container, start the Smart Rollup node: ```bash octez-smart-rollup-node run operator for "test_smart_rollup" \ with operators "bootstrap2" --data-dir ~/.tezos-rollup-node/ \ --log-kernel-debug --log-kernel-debug-file hello_kernel.debug ``` Now the node is running and writing to the log file `hello_kernel.debug`. Leave this command running in the terminal window just like you left the first terminal window running the Tezos sandbox. ## Interacting with the Smart Rollup node Now you can add messages to the inbox and see the Smart Rollup node receive and respond to them. 1. Open a third terminal window and enter the Docker container again: ```bash docker exec -it octez-container /bin/sh ``` 2. In the container, go to the `hello_world_kernel` folder. 3. Print the contents of the log file: ```bash tail -f hello_kernel.debug ``` Now, each time a block is baked, the Smart Rollup node prints the contents of the messages in the Smart Rollup inbox, as in this example: ``` # Hello, kernel! # Got message: Internal(StartOfLevel) # Got message: Internal(InfoPerLevel(InfoPerLevel { predecessor_timestamp: 2023-06-07T15:31:09Z, predecessor: BlockHash("BLQucC2rFyNhoeW4tuh1zS1g6H6ukzs2DQDUYArWNALGr6g2Jdq") })) # Got message: Internal(EndOfLevel) ``` 4. Stop the command by pressing Ctrl + C. 5. Run this command to watch for external messages to the rollup: ```bash tail -f hello_kernel.debug | grep External ``` No output appears at first because the rollup has not received any messages aside from the internal messages that indicate the beginning and end of the inbox. Leave this command running. 6. Open a fourth terminal window, enter the Docker container with the command `docker exec -it octez-container /bin/sh`, and go to the `hello_world_kernel` folder. 7. In this fourth terminal window, run this command to simulate adding a message to the Smart Rollup inbox: ```bash octez-client send smart rollup message '[ "test" ]' from "bootstrap3" ``` 8. Go back to the third terminal window. This window shows the message that you sent in the fourth window, which it received in binary format, with the numbers representing the letters in "test." ``` Got message: External([116, 101, 115, 116]) ``` Now you can send messages to this rollup via Tezos layer 1 and act on them in the rollup code. ## Next steps To continue your work with Smart Rollups, you can you can explore examples from the [kernel gallery](https://gitlab.com/tezos/kernel-gallery/-/tree/main/) or create your own. ## References * [Smart Rollup documentation](https://octez.tezos.com/docs/active/smart_rollups.html) * [Smart Rollup kernel SDK](https://gitlab.com/tezos/tezos/-/tree/master/src/kernel_sdk) * [Smart Rollup kernel examples](https://gitlab.com/tezos/kernel-gallery/-/tree/main/) * [Tezos Smart Rollups resources](https://airtable.com/shrvwpb63rhHMiDg9/tbl2GNV1AZL4dkGgq) * [Tezos testnets](https://teztnets.com/) * [Originating the installer kernel](https://tezos.stackexchange.com/questions/4784/how-to-originating-a-smart-rollup-with-an-installer-kernel/5794#5794) * [Docker documentation](https://docs.docker.com/get-started/) # Build an NFT marketplace Estimated time: 3 hours :::warning This tutorial is currently broken, because it depends on Taqueria, which has not been updated for the latest version of LIGO. As soon as that issue is fixed, the tutorial will be made available again, on the Shadownet testnet. ::: This tutorial guides you through creating a web application that allows users to buy and sell tokens of different types. You will use the Taqueria platform to manage smart contracts and a distributed web application (dApp) to handle the backend and frontend of the project. You will learn: * What kinds of tokens Tezos supports * What token standards are * How to create contracts that are based on existing templates for token standards * How to store token metadata in distributed storage with IPFS * How to handle token transfers and other operations * How to list tokens for sale and accept payments from buyers ## Prerequisites 1. Optional: If you haven't worked with Tezos NFTs before, consider doing the tutorial [Create NFTs from a web application](/tutorials/create-nfts) first. 2. Set up an account with Pinata if you don't have one already and get an API key and API secret. For information about setting up a Pinata account, see [Storing data and files with IPFS](/developing/ipfs). 3. Make sure that you have installed these tools: * [Node.JS and NPM](https://nodejs.org/en/download/): Node.js version 22.12.0 or later and NPM are required to install the web application's dependencies * [Taqueria](https://taqueria.io/), version 0.78.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, such as [Temple wallet](https://templewallet.com/); see [Installing and funding a wallet](/developing/wallet-setup) 4. 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. 5. Optional: If this is your first time using Taqueria, you may want to run through [this Taqueria training](https://github.com/marigold-dev/training-dapp-1) first. ## What are FA2 tokens? If you've gone through the tutorial [Create NFTs from a web application](/tutorials/create-nfts) you know that NFTs are blockchain tokens that represent unique assets, usually created under the FA2 token standard. However, the Tezos FA2 token standard allows you to create multiple types of tokens, and even more than one kind of token within the same smart contract. When you create tokens, it's important to follow one of the token standards because then tools like wallets and block explorers can automatically work with those tokens. For more information about Tezos token standards, see [Token standards](/architecture/tokens). In this tutorial, you use the LIGO template for FA2 tokens to create these types of tokens: | Token template | Number of token types | Number of tokens of each type | | -------------- | --------------------- | ----------------------------- | | NFT | Any number | 1 | | Single-asset | 1 | Any number | | Multi-asset | Any number | Any number | When you create your own applications, you can choose the token type that is appropriate for your use case. ## What is IPFS? In most cases, developers don't store token metadata such as image files directly on Tezos. Instead, they configure decentralized storage for the NFT data and put only the link to that data on Tezos itself. The InterPlanetary File System (IPFS) is a protocol and peer-to-peer network for storing and sharing data in a distributed file system. IPFS uses content-addressable storage to uniquely identify each file in a global namespace connecting all computing devices. In this tutorial, you use [Pinata](https://www.pinata.cloud/)'s free developer plan to store your NFT metadata on IPFS and reference it on Tezos, demonstrating a scalable and cost-effective solution for handling NFT data. ## Tutorial application This tutorial was originally created by Marigold, which hosts versions of the tutorial application after each part of the tutorial: * [Part 1](https://github.com/marigold-dev/training-nft-1) * [Part 2](https://github.com/marigold-dev/training-nft-2) * [Part 3](https://github.com/marigold-dev/training-nft-3) * [Part 4](https://github.com/marigold-dev/training-nft-4) The completed application at the end of the tutorial is a marketplace where administrator users can list wine bottles for sale by entering information about them and uploading a photo. The application creates tokens based on this information and the site allows other users to buy the tokens that represent wine bottles. ![The complete application, showing wine bottles for sale](/img/tutorials/nftfactory.png) This application is made up of a smart contract that handles the tokens and a frontend web application that handles the user interface and sends transactions to the backend. As you work through the tutorial, you will use different smart contracts and upgrade the web application to work with them. When you're ready, go to [Part 1: Minting tokens](/tutorials/build-an-nft-marketplace/part-1) to begin. # Part 1: Minting tokens To start working with the application, you create a Taqueria project and use it to deploy an FA2 contract. Then you set up a web application to mint NFTs by calling the contract's mint endpoint and uploading an image and metadata to IPFS. :::note Before you begin, make sure that you have installed the tools in the [Prerequisites](/tutorials/build-an-nft-marketplace#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: 1. On the command-line terminal, run these commands to set up a Taqueria project and install the LIGO and Taquito plugins: ```bash taq init nft-marketplace cd nft-marketplace taq install @taqueria/plugin-ligo taq install @taqueria/plugin-taquito ``` 2. Install the `ligo/fa` library, which provides templates for creating FA2 tokens: ```bash echo '{ "name": "app", "dependencies": { "@ligo/fa": "^1.4.2" } }' >> ligo.json TAQ_LIGO_IMAGE=ligolang/ligo:1.10.0 taq ligo --command "install @ligo/fa" ``` This command can take some time because it downloads and installs the `@ligo/fa` package. 3. Run one of these commands to accept or decline LIGO's analytics policy: * `ligo analytics accept` to send analytics data to LIGO * `ligo analytics deny` to not send analytics data to LIGO ## Creating an FA2 contract from a template The `ligo/fa` library provides a template that saves you from having to implement all of the FA2 standard yourself. Follow these steps to create a contract that is based on the template and implements the required endpoints: 1. Create a contract to manage your NFTs: ```bash taq create contract nft.jsligo ``` 2. Open the `contracts/nft.jsligo` file in any text editor and replace the default code with this code: ```jsligo #import "@ligo/fa/lib/fa2/nft/extendable_nft.impl.jsligo" "FA2Impl" /* ERROR MAP FOR UI DISPLAY or TESTS const errorMap : map = Map.literal(list([ ["0", "Enter a positive and not null amount"], ["1", "Operation not allowed, you need to be administrator"], ["2", "You cannot sell more than your current balance"], ["3", "Cannot find the offer you entered for buying"], ["4", "You entered a quantity to buy than is more than the offer quantity"], ["5", "Not enough funds, you need to pay at least quantity * offer price to get the tokens"], ["6", "Cannot find the contract relative to implicit address"], ])); */ export type Extension = { administrators: set
}; export type storage = FA2Impl.storage; // extension administrators type ret = [list, storage]; ``` The first line of this code imports the FA2 template as the `FA2Impl` object. Then, the code defines error messages for the contract. The code defines a type for the contract storage, which contains these values: * `administrators`: A list of accounts that are authorized to mint NFTs * `ledger`: The ledger that keeps track of token ownership * `metadata`: The metadata for the contract itself, based on the TZIP-16 standard for contract metadata * `token_metadata`: The metadata for the tokens, based on the TZIP-12 standard for token metadata * `operators`: Information about *operators*, accounts that are authorized to transfer tokens on behalf of the owners The code also defines the type for the value that entrypoints return: a list of operations and the new value of the storage. 3. Add code to implement the required `transfer`, `balance_of`, and `update_operators` entrypoints: ```jsligo @entry const transfer = (p: FA2Impl.TZIP12.transfer, s: storage): ret => { const ret2: [list, storage] = FA2Impl.transfer(p, s); return [ ret2[0], { ...s, ledger: ret2[1].ledger, metadata: ret2[1].metadata, token_metadata: ret2[1].token_metadata, operators: ret2[1].operators, } ] }; @entry const balance_of = (p: FA2Impl.TZIP12.balance_of, s: storage): ret => { const ret2: [list, storage] = FA2Impl.balance_of(p, s); return [ ret2[0], { ...s, ledger: ret2[1].ledger, metadata: ret2[1].metadata, token_metadata: ret2[1].token_metadata, operators: ret2[1].operators, } ] }; @entry const update_operators = (p: FA2Impl.TZIP12.update_operators, s: storage): ret => { const ret2: [list, storage] = FA2Impl.update_operators(p, s); return [ ret2[0], { ...s, ledger: ret2[1].ledger, metadata: ret2[1].metadata, token_metadata: ret2[1].token_metadata, operators: ret2[1].operators, } ] }; ``` You will add other entrypoints later, but these are the three entrypoints that every FA2 contract must have. Because these required entrypoints must have specific parameters, the code re-uses types from the `FA2Impl` object for those parameters. For example, the `FA2Impl.TZIP12.transfer` type represents the parameters for transferring tokens, including a source account and a list of target accounts, token types, and amounts. * The `transfer` entrypoint accepts information about the tokens to transfer. This implementation uses the `FA2Impl.NFT.transfer` function from the template to avoid having to re-implement what happens when tokens are transferred. * The `balance_of` entrypoint sends information about an owner's token balance to another contract. This implementation re-uses the `FA2Impl.NFT.balance_of` function. * The `update_operators` entrypoint updates the operators for a specified account. This implementation re-uses the `FA2Impl.NFT.update_operators` function. 4. After those entrypoints, add code for the `mint` entrypoint: ```jsligo @entry const mint = ( [token_id, name, description, symbol, ipfsUrl]: [ nat, bytes, bytes, bytes, bytes ], s: storage ): ret => { if (! Set.mem(Tezos.get_sender(), s.extension.administrators)) return failwith( "1" ); const token_info: map = Map.literal( list( [ ["name", name], ["description", description], ["interfaces", (bytes `["TZIP-12"]`)], ["artifactUri", ipfsUrl], ["displayUri", ipfsUrl], ["thumbnailUri", ipfsUrl], ["symbol", symbol], ["decimals", (bytes `0`)] ] ) ) as map; return [ list([]) as list, { ...s, ledger: Big_map.add(token_id, Tezos.get_sender(), s.ledger) as FA2Impl.ledger, token_metadata: Big_map.add( token_id, { token_id: token_id, token_info: token_info }, s.token_metadata ), operators: Big_map.empty as FA2Impl.operators, } ] }; ``` The FA2 standard does not require a mint entrypoint, but you can add one if you want to allow the contract to create more tokens after it is originated. If you don't include a mint entrypoint or a way to create tokens, you must initialize the storage with all of the token information when you originate the contract. This mint entrypoint accepts a name, description, symbol, and IPFS URL to an image. It also accepts an ID number for the token, which the front end will manage; you could also set up the contract to remember the ID number for the next token. First, this code verifies that the transaction sender is one of the administrators. Then it creates a token metadata object with information from the parameters and adds it to the `token_metadata` big-map in the storage. Note that the `decimals` metadata field is set to 0 because the token is an NFT and therefore doesn't need any decimal places in its quantity. Note that there is no built-in way to get the number of tokens in the contract code; the Bigmap does not have a function such as `keys()` or `length()`. If you want to keep track of the number of tokens, you must add an element in the storage and increment it when tokens are created or destroyed. You can also get the number of tokens by analyzing the contract's storage from an off-chain application. 5. Save the contract and compile it by running this command: ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.10.0 taq compile nft.jsligo ``` Taqueria compiles the contract to the file `artifacts/nft.tz`. It also creates the file `nft.storageList.jsligo`, which contains the starting value of the contract storage. :::note This command shows deprecation warnings that start with "In a future version." These warnings refer to files in the FA2 library; you can safely ignore them. ::: 6. Open the file `contracts/nft.storageList.jsligo` and replace it with this code: ```jsligo #import "nft.jsligo" "Contract" #import "@ligo/fa/lib/fa2/nft/extendable_nft.impl.jsligo" "FA2Impl" const default_storage: Contract.storage = { extension: { administrators: Set.literal( list(["tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb" as address]) ) as set
}, ledger: Big_map.empty as FA2Impl.ledger, metadata: Big_map.literal( list( [ ["", bytes `tezos-storage:data`], [ "data", bytes `{ "name":"FA2 NFT Marketplace", "description":"Example of FA2 implementation", "version":"0.0.1", "license":{"name":"MIT"}, "authors":["Marigold"], "homepage":"https://marigold.dev", "source":{ "tools":["Ligo"], "location":"https://github.com/ligolang/contract-catalogue/tree/main/lib/fa2"}, "interfaces":["TZIP-012"], "errors": [], "views": [] }` ] ] ) ) as FA2Impl.TZIP16.metadata, token_metadata: Big_map.empty as FA2Impl.TZIP12.tokenMetadata, operators: Big_map.empty as FA2Impl.operators, }; ``` This code sets the initial value of the storage. In this case, the storage includes metadata about the contract and empty Bigmaps for the ledger, token metadata, and operators. It sets the test account Alice as the administrator, which is the only account that can mint tokens. 7. Optional: Add your address as an administrator or replace Alice's address with your own. Note that only the addresses in the `administrators` list will be able to create tokens. 8. Compile the contract: ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.10.0 taq compile nft.jsligo ``` 9. Use one of these options to set up a Shadownet account to use to deploy (originate) the contract: * To use your account, open the `.taq/config.local.testing.json` file and add your public key, address, and private key, so the file looks like this: ```json { "networkName": "shadownet", "accounts": { "taqOperatorAccount": { "publicKey": "edpkvGfYw3LyB1UcCahKQk4rF2tvbMUk8GFiTuMjL75uGXrpvKXhjn", "publicKeyHash": "tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb", "privateKey": "edsk3QoqBuvdamxouPhin7swCvkQNgq4jP5KZPbwWNnwdZpSpJiEbq" } } } ``` Then make sure that the account has tez on Shadownet. Use the faucet at https://faucet.shadownet.teztnets.com to get tez if you need it. **OR** * To let Taqueria generate an account for you, follow these steps: 1. Run the command `taq deploy nft.tz -e "testing"`, which will fail because you do not have an account configured in Taqueria. The response includes the address of an account that Taqueria generated for you and added to the `.taq/config.local.testing.json` file automatically. 2. Fund the account from the faucet at https://faucet.shadownet.teztnets.com. 10. Compile and deploy the contract to Shadownet by running this command: ```bash taq deploy nft.tz -e "testing" ``` Taqueria deploys the contract to Shadownet and prints the address of the contract, as in this image: ![The output of the deployment command](/img/tutorials/taqueria-contract-deploy-result.png) If you see an error that says "No initial storage file was found for nft.tz," make sure that you compiled the contract after updating the storage file. Now the backend application is ready and you can start on the frontend application. ## Creating the frontend application To save time, this tutorial provides a starter Vite/React application for the frontend. 1. In your Taqueria project, create a folder named `app` that is at the same level as the `contracts` folder. 2. In a folder outside of your Taqueria project, clone the source material and completed project files by running this command: ```bash git clone https://github.com/trilitech/tutorial-applications.git ``` This repository has a folder named `nft-marketplace` that includes the starter application and the completed applications for each part of this tutorial, which you can refer to later. 3. From the repository, copy the contents of the `nft-marketplace/frontend-starter/` folder to the `app` folder. For information about how this starter application was created, see the "Dapp" section of this tutorial: https://github.com/marigold-dev/training-dapp-1#construction_worker-dapp. 4. From the root of your Taqueria project, run these commands to generate TypeScript types for the application: ```bash taq install @taqueria/plugin-contract-types taq generate types ./app/src ``` 5. **IF YOU ARE ON A MAC**, edit the default `dev` script in the `app/package.json` file to look like this: ```json { "scripts": { "dev": "if test -f .env; then sed -i '' \"s/\\(VITE_CONTRACT_ADDRESS *= *\\).*/\\1$(jq -r 'last(.tasks[]).output[0].address' ../.taq/testing-state.json)/\" .env ; else jq -r '\"VITE_CONTRACT_ADDRESS=\" + last(.tasks[]).output[0].address' ../.taq/testing-state.json > .env ; fi && vite" } } ``` This is required on Mac computers because the `sed` command behaves differently than on Unix computers. 6. Run these commands to install the dependencies for the application and start it: ```bash cd app yarn && yarn dev ``` 7. In the file `app/.env`, set the variables `VITE_PINATA_API_KEY` and `VITE_PINATA_API_SECRET` to your Pinata API key and API secret. For information about setting up a Pinata account, see [Storing data and files with IPFS](/developing/ipfs). 8. In the file `app/.env`, add this line: ```bash VITE_TEZOS_NODE=https://rpc.shadownet.teztnets.com ``` The file looks like this, with your contract addresses and Pinata information in the variables: ```bash VITE_CONTRACT_ADDRESS=KT1HZzcAJRqANWWCt23yBdZ7T7DByLYkws44 VITE_PINATA_API_KEY=abc123 VITE_PINATA_API_SECRET=def456 VITE_TEZOS_NODE=https://rpc.shadownet.teztnets.com ``` 9. Open the application in a web browser at http://localhost:5173. This application contains basic navigation and the ability to connect to wallets. For a tutorial that includes connecting to wallets, see [Build a simple web application](/tutorials/build-your-first-app). The application shows a blank landing page that looks like this: ![The starter NFT marketplace application is showing no NFTs and a button to connect to wallets](/img/tutorials/nft-marketplace-starter.png) ## Adding a mint page The mint page uses a form that accepts information and an image and sends a transaction to the mint entrypoint: 1. Open the file `app/src/MintPage.tsx`. 2. Replace the return value of the function (the `` tag) with the following code: ```html {storage ? ( ) : ( "" )}
Mint a new collection {pictureUrl ? ( ) : ( "" )}
Mint your wine collection {nftContractTokenMetadataMap.size != 0 ? ( "//TODO" ) : ( No NFTs yet. Click "Connect wallet" and mint an NFT. )}
``` You may see errors in your IDE for missing code and imports that you will add later. This code shows an HTML form if the connected wallet is an administrator. The form includes fields for a new NFT, including a button to upload an image. 3. Inside the `MintPage` function, immediately before the `return` statement, add this [Formik](https://formik.org/) form to manage the form: ```typescript const validationSchema = yup.object({ name: yup.string().required('Name is required'), description: yup.string().required('Description is required'), symbol: yup.string().required('Symbol is required'), }); const formik = useFormik({ initialValues: { name: '', description: '', token_id: 0, symbol: 'WINE', } as TZIP21TokenMetadata, validationSchema: validationSchema, onSubmit: (values) => { mint(values); }, }); ``` 4. After this code, add state variables for the image and its URL: ```typescript const [pictureUrl, setPictureUrl] = useState(''); const [file, setFile] = useState(null); ``` 5. Add this code to manage a drawer that appears to show the form: ```typescript //open mint drawer if admin const [formOpen, setFormOpen] = useState(false); useEffect(() => { if (storage && storage.extension.indexOf(userAddress! as address) < 0) setFormOpen(false); else setFormOpen(true); }, [userAddress]); const toggleDrawer = (open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => { if ( event.type === 'keydown' && ((event as React.KeyboardEvent).key === 'Tab' || (event as React.KeyboardEvent).key === 'Shift') ) { return; } setFormOpen(open); }; ``` 6. Add this `mint` function: ```typescript const { enqueueSnackbar } = useSnackbar(); const mint = async (newTokenDefinition: TZIP21TokenMetadata) => { try { //IPFS if (file) { const formData = new FormData(); formData.append('file', file); const requestHeaders: HeadersInit = new Headers(); requestHeaders.set( 'pinata_api_key', `${import.meta.env.VITE_PINATA_API_KEY}` ); requestHeaders.set( 'pinata_secret_api_key', `${import.meta.env.VITE_PINATA_API_SECRET}` ); const resFile = await fetch( 'https://api.pinata.cloud/pinning/pinFileToIPFS', { method: 'post', body: formData, headers: requestHeaders, } ); const responseJson = await resFile.json(); console.log('responseJson', responseJson); const thumbnailUri = `ipfs://${responseJson.IpfsHash}`; setPictureUrl( `https://gateway.pinata.cloud/ipfs/${responseJson.IpfsHash}` ); const op = await nftContract!.methods .mint( new BigNumber(newTokenDefinition.token_id) as nat, char2Bytes(newTokenDefinition.name!) as bytes, char2Bytes(newTokenDefinition.description!) as bytes, char2Bytes(newTokenDefinition.symbol!) as bytes, char2Bytes(thumbnailUri) as bytes ) .send(); //close directly the form setFormOpen(false); enqueueSnackbar( 'Wine collection is minting ... it will be ready on next block, wait for the confirmation message before minting another collection', { variant: 'info' } ); await op.confirmation(2); enqueueSnackbar('Wine collection minted', { variant: 'success' }); refreshUserContextOnPageReload(); //force all app to refresh the context } } catch (error) { console.table(`Error: ${JSON.stringify(error, null, 2)}`); let tibe: TransactionInvalidBeaconError = new TransactionInvalidBeaconError(error); enqueueSnackbar(tibe.data_message, { variant: 'error', autoHideDuration: 10000, }); } }; ``` This function accepts the data that the user puts in the form. It uploads the image to IPFS via Pinata and gets the IPFS hash, which identifies the published file and allows clients to request it later. Then it calls the contract's `mint` entrypoint and passes the NFT data as bytes, as the TZIP-12 standard requires for NFT metadata. 7. Add code to set the ID for the next NFT based on the number of tokens currently in the contract: ```typescript useEffect(() => { (async () => { if (nftContractTokenMetadataMap && nftContractTokenMetadataMap.size > 0) { formik.setFieldValue('token_id', nftContractTokenMetadataMap.size); } })(); }, [nftContractTokenMetadataMap?.size]); ``` 8. Replace the imports at the top of the file with these imports: ```typescript import { AddCircleOutlined, Close } from '@mui/icons-material'; import OpenWithIcon from '@mui/icons-material/OpenWith'; import { Box, Button, Stack, SwipeableDrawer, TextField, Toolbar, useMediaQuery, } from '@mui/material'; import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; import { useFormik } from 'formik'; import React, { useEffect, useState } from 'react'; import * as yup from 'yup'; import { TZIP21TokenMetadata, UserContext, UserContextType } from './App'; import { useSnackbar } from 'notistack'; import { BigNumber } from 'bignumber.js'; import { address, bytes, nat } from './type-aliases'; import { char2Bytes } from '@taquito/utils'; import { TransactionInvalidBeaconError } from './TransactionInvalidBeaconError'; ``` 9. Save the file. Now the form has a working mint page. In the next section, you use it to mint NFTs. ## Minting NFTs Mint at least one NFT so you can see it in the site and contract: 1. Open the site by going to http://localhost:5173 in your web browser. If the site isn't running, go to the `app` folder and run `yarn dev`. 2. Connect the administrator's wallet to the application. The app goes to the `/mint` page, which looks like this: ![The mint page shows the form to create tokens](/img/tutorials/nft-marketplace-1-mint-form.png) If the app does not go directly to the mint page, click "Mine wine connection" on the left side of the page. 3. Enter information about a bottle of wine. For example, you can use this information: * `name`: Saint Emilion - Franc la Rose * `symbol`: SEMIL * `description`: Grand cru 2007 4. Upload a picture to represent a bottle of wine. You can use the file at https://github.com/trilitech/tutorial-applications/tree/main/nft-marketplace/winebottle.png or use one of your own. 5. Click **Mint**. 6. Approve the transaction in your wallet and wait for it to complete. ![Waiting for confirmation that the NFT was minted](/img/tutorials/nft-marketplace-1-minting.png) When the NFT has been minted, the application updates the UI but it does not have code to show the NFTs yet. You can see the NFT by getting the contract address, which starts with `KT1`, from the `config.local.testing.json` file or `app/.env` file and looking it up in a block explorer. For example, this is how https://shadownet.tzkt.io/ shows the tokens in the contract, on the "Tokens" tab. Because the contract is FA2-compatible, the block explorer automatically shows information about the tokens: ![The TzKT block explorer is showing the token in the contract](/img/tutorials/nft-marketplace-1-tzkt-token.png) Now the application can mint NFTs. In the next section, you display the NFTs on a catalog page. ## Displaying tokens Follow these steps to show the tokens that you have minted: 1. In the `app/src/MintPage.tsx` file, replace the `"//TODO"` comment with this code: ```typescript {Array.from(nftContractTokenMetadataMap!.entries()).map( ([token_id, token]) => ( {'ID : ' + token_id} {'Symbol : ' + token.symbol} {'Description : ' + token.description} ) )} Next } backButton={ } /> ``` This code gets data from the contract storage and shows it on the UI. 2. Add these constants in the `MintPage` function: ```typescript const [activeStep, setActiveStep] = React.useState(0); const handleNext = () => { setActiveStep((prevActiveStep) => prevActiveStep + 1); }; const handleBack = () => { setActiveStep((prevActiveStep) => prevActiveStep - 1); }; const handleStepChange = (step: number) => { setActiveStep(step); }; ``` 3. Replace the imports at the top of the file with these imports: ```typescript import SwipeableViews from 'react-swipeable-views'; import OpenWithIcon from '@mui/icons-material/OpenWith'; import { Box, Button, CardHeader, CardMedia, MobileStepper, Stack, SwipeableDrawer, TextField, Toolbar, useMediaQuery, } from '@mui/material'; import Card from '@mui/material/Card'; import CardContent from '@mui/material/CardContent'; import { AddCircleOutlined, Close, KeyboardArrowLeft, KeyboardArrowRight, } from '@mui/icons-material'; import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; import { useFormik } from 'formik'; import React, { useEffect, useState } from 'react'; import * as yup from 'yup'; import { TZIP21TokenMetadata, UserContext, UserContextType } from './App'; import { useSnackbar } from 'notistack'; import { BigNumber } from 'bignumber.js'; import { address, bytes, nat } from './type-aliases'; import { char2Bytes } from '@taquito/utils'; import { TransactionInvalidBeaconError } from './TransactionInvalidBeaconError'; ``` 4. Open the web page in the browser again and see that the NFT you created is shown, as in this picture: ![The mint page showing one existing NFT](/img/tutorials/nft-marketplace-1-collection.png) For the complete content of the mint page, see the completed part 1 app at https://github.com/trilitech/tutorial-applications/tree/main/nft-marketplace/part-1. ## Summary Now you can create FA2-compatible NFTs with the `@ligo/fa` library and show them on a web page. In the next section, you add the buy and sell functions to the smart contract and update the frontend application to allow these actions. When you are ready, continue to [Part 2: Buying and selling tokens](/tutorials/build-an-nft-marketplace/part-2). # Part 2: Buying and selling tokens In this section, you give users the ability to list a bottle for sale and buy bottles that are listed for sale. You can continue from your code from part 1 or download the completed app from the end of part 1 here: https://github.com/trilitech/tutorial-applications/tree/main/nft-marketplace/part-1. If you start from the completed version, run these commands to install dependencies for the web application: ```bash npm i cd ./app yarn install cd .. ``` ## Updating the smart contract To allow users to buy and sell tokens, the contract must have entrypoints that allow users to offer a token for sale and to buy a token that is offered for sale. The contract storage must store the tokens that are offered for sale and their prices. 1. Update the contract storage to store the tokens that are offered for sale: 1. In the `contracts/nft.jsligo` file, before the definition of the `storage` and `Extension` types, add a type that represents a token that is offered for sale: ```jsligo export type offer = { owner : address, price : nat }; ``` 2. Add a map named `offers` that maps token IDs to their offer prices to the `Extension` type. Now the `Extension` type looks like this: ```jsligo export type Extension = { administrators: set
, offers: map, //user sells an offer }; ``` 3. In the `contracts/nft.storageList.jsligo` file, add an empty map for the offers as a field of the `extension` object by adding this code: ```jsligo offers: Map.empty as map ``` Now the `nft.storageList.jsligo` file looks like this, with your address in the `administrators` list: ```jsligo #import "nft.jsligo" "Contract" #import "@ligo/fa/lib/fa2/nft/extendable_nft.impl.jsligo" "FA2Impl" const default_storage: Contract.storage = { extension: { administrators: Set.literal( list(["tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb" as address]) ) as set
, offers: Map.empty as map }, ledger: Big_map.empty as FA2Impl.ledger, metadata: Big_map.literal( list( [ ["", bytes `tezos-storage:data`], [ "data", bytes `{ "name":"FA2 NFT Marketplace", "description":"Example of FA2 implementation", "version":"0.0.1", "license":{"name":"MIT"}, "authors":["Marigold"], "homepage":"https://marigold.dev", "source":{ "tools":["Ligo"], "location":"https://github.com/ligolang/contract-catalogue/tree/main/lib/fa2"}, "interfaces":["TZIP-012"], "errors": [], "views": [] }` ] ] ) ) as FA2Impl.TZIP16.metadata, token_metadata: Big_map.empty as FA2Impl.TZIP12.tokenMetadata, operators: Big_map.empty as FA2Impl.operators, }; ``` ``` ``` 2. As you did in the previous step, make sure that the administrators in the `nft.storageList.jsligo` file includes an address that you can use to mint tokens. 3. In the `contracts/nft.jsligo` file, add a `sell` entrypoint that creates an offer for a token that the sender owns: ```jsligo @entry const sell = ([token_id, price]: [nat, nat], s: storage): ret => { //check balance of seller const sellerBalance = FA2Impl.get_balance([Tezos.get_source(), token_id], s); if (sellerBalance != (1 as nat)) return failwith("2"); //need to allow the contract itself to be an operator on behalf of the seller const newOperators = FA2Impl.add_operator( s.operators, Tezos.get_source(), Tezos.get_self_address(), token_id ); //DECISION CHOICE: if offer already exists, we just override it return [ list([]) as list, { ...s, extension: { ...s.extension, offers: Map.add( token_id, { owner: Tezos.get_source(), price: price }, s.extension.offers ) }, operators: newOperators } ] }; ``` This function accepts the ID of the token and the selling price as parameters. It verifies that the transaction sender owns the token. Then it adds the contract itself as an operator of the token, which allows it to transfer the token without getting permission from the seller later. Finally, it adds the offer and updated operators to the storage. 4. Add a `buy` entrypoint: ```jsligo @entry const buy = ([token_id, seller]: [nat, address], s: storage): ret => { //search for the offer return match(Map.find_opt(token_id, s.extension.offers)) { when (None()): failwith("3") when (Some(offer)): do { //check if amount have been paid enough if (Tezos.get_amount() < offer.price * (1 as mutez)) return failwith( "5" ); // prepare transfer of XTZ to seller const op = Tezos.transaction( unit, offer.price * (1 as mutez), Tezos.get_contract_with_error(seller, "6") ); //transfer tokens from seller to buyer const ledger = FA2Impl.transfer_token_from_user_to_user( s.ledger, token_id, seller, Tezos.get_source() ); //remove offer return [ list([op]) as list, { ...s, ledger: ledger, extension: { ...s.extension, offers: Map.update(token_id, None(), s.extension.offers), } } ] } } }; ``` This entrypoint accepts the token ID and seller as parameters. It retrieves the offer from storage and verifies that the transaction sender sent enough tez to satisfy the offer price. Then it transfers the token to the buyer, transfers the sale price to the seller, and removes the offer from storage. 5. Compile and deploy the new contract: ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.10.0 taq compile nft.jsligo taq deploy nft.tz -e "testing" ``` 6. Generate the TypeScript classes from the contract: ```bash taq generate types ./app/src ``` ## Adding a selling page to the web application 1. Stop the web application if it is running. 2. Start the server: ```bash cd ./app yarn dev ``` 3. On the mint page in the `app/src/MintPage.tsx` file, fix all **extension** relative errors by replacing `storage.extension` with `storage.extension.administrators` in each occurrence. 4. Similarly, replace `storage!.extension` with `storage!.extension.administrators`. 5. Open the sale page in the `app/src/OffersPage.tsx` file and replace it with this code: ```typescript import { InfoOutlined } from '@mui/icons-material'; import SellIcon from '@mui/icons-material/Sell'; import * as api from '@tzkt/sdk-api'; import { Box, Button, Card, CardActions, CardContent, CardHeader, CardMedia, ImageList, InputAdornment, Pagination, TextField, Tooltip, Typography, useMediaQuery, } from '@mui/material'; import Paper from '@mui/material/Paper'; import BigNumber from 'bignumber.js'; import { useFormik } from 'formik'; import { useSnackbar } from 'notistack'; import React, { Fragment, useEffect, useState } from 'react'; import * as yup from 'yup'; import { UserContext, UserContextType } from './App'; import ConnectButton from './ConnectWallet'; import { TransactionInvalidBeaconError } from './TransactionInvalidBeaconError'; import { address, nat } from './type-aliases'; const itemPerPage: number = 6; const validationSchema = yup.object({ price: yup .number() .required('Price is required') .positive('ERROR: The number must be greater than 0!'), }); type Offer = { owner: address; price: nat; }; export default function OffersPage() { api.defaults.baseUrl = 'https://api.shadownet.tzkt.io'; const [selectedTokenId, setSelectedTokenId] = React.useState(0); const [currentPageIndex, setCurrentPageIndex] = useState(1); let [offersTokenIDMap, setOffersTokenIDMap] = React.useState< Map >(new Map()); let [ownerTokenIds, setOwnerTokenIds] = React.useState>( new Set() ); const { nftContract, nftContractTokenMetadataMap, userAddress, storage, refreshUserContextOnPageReload, Tezos, setUserAddress, setUserBalance, wallet, } = React.useContext(UserContext) as UserContextType; const { enqueueSnackbar } = useSnackbar(); const formik = useFormik({ initialValues: { price: 0, }, validationSchema: validationSchema, onSubmit: (values) => { console.log('onSubmit: (values)', values, selectedTokenId); sell(selectedTokenId, values.price); }, }); const initPage = async () => { if (storage) { console.log('context is not empty, init page now'); ownerTokenIds = new Set(); offersTokenIDMap = new Map(); const token_metadataBigMapId = ( storage.token_metadata as unknown as { id: BigNumber } ).id.toNumber(); const token_ids = await api.bigMapsGetKeys(token_metadataBigMapId, { micheline: 'Json', active: true, }); await Promise.all( token_ids.map(async (token_idKey) => { const token_idNat = new BigNumber(token_idKey.key) as nat; let owner = await storage.ledger.get(token_idNat); if (owner === userAddress) { ownerTokenIds.add(token_idKey.key); const ownerOffers = await storage.extension.offers.get( token_idNat ); if (ownerOffers) offersTokenIDMap.set(token_idKey.key, ownerOffers); console.log( 'found for ' + owner + ' on token_id ' + token_idKey.key + ' with balance ' + 1 ); } else { console.log('skip to next token id'); } }) ); setOwnerTokenIds(new Set(ownerTokenIds)); //force refresh setOffersTokenIDMap(new Map(offersTokenIDMap)); //force refresh } else { console.log('context is empty, wait for parent and retry ...'); } }; useEffect(() => { (async () => { console.log('after a storage changed'); await initPage(); })(); }, [storage]); useEffect(() => { (async () => { console.log('on Page init'); await initPage(); })(); }, []); const sell = async (token_id: number, price: number) => { try { const op = await nftContract?.methods .sell( BigNumber(token_id) as nat, BigNumber(price * 1000000) as nat //to mutez ) .send(); await op?.confirmation(2); enqueueSnackbar( 'Wine collection (token_id=' + token_id + ') offer for ' + 1 + ' units at price of ' + price + ' XTZ', { variant: 'success' } ); refreshUserContextOnPageReload(); //force all app to refresh the context } catch (error) { console.table(`Error: ${JSON.stringify(error, null, 2)}`); let tibe: TransactionInvalidBeaconError = new TransactionInvalidBeaconError(error); enqueueSnackbar(tibe.data_message, { variant: 'error', autoHideDuration: 10000, }); } }; const isDesktop = useMediaQuery('(min-width:1100px)'); const isTablet = useMediaQuery('(min-width:600px)'); return ( Sell my bottles {ownerTokenIds && ownerTokenIds.size != 0 ? ( setCurrentPageIndex(value)} count={Math.ceil( Array.from(ownerTokenIds.entries()).length / itemPerPage )} showFirstButton showLastButton /> {Array.from(ownerTokenIds.entries()) .filter((_, index) => index >= currentPageIndex * itemPerPage - itemPerPage && index < currentPageIndex * itemPerPage ? true : false ) .map(([token_id]) => ( {' '} {'ID : ' + token_id.toString()}{' '} {'Description : ' + nftContractTokenMetadataMap.get(token_id) ?.description} } > } title={nftContractTokenMetadataMap.get(token_id)?.name} /> {offersTokenIDMap.get(token_id) ? 'Traded : ' + 1 + ' (price : ' + offersTokenIDMap .get(token_id) ?.price.dividedBy(1000000) + ' Tz)' : ''} {!userAddress ? ( ) : (
{ setSelectedTokenId(Number(token_id)); formik.handleSubmit(values); }} > ), }} />
)}
))}{' '}
) : ( Sorry, you don't own any bottles, buy or mint some first )}
); } ``` This page shows the bottles that the connected account owns and allows the user to select bottles for sale. When the user selects bottles and adds a sale price, the page calls the `sell` entrypoint with this code: ```typescript nftContract?.methods .sell(BigNumber(token_id) as nat, BigNumber(price * 1000000) as nat) .send(); ``` This code multiplies the price by 1,000,000 because the UI shows prices in tez but the contract records prices in mutez. Then the contract creates an offer for the selected token. 6. As you did in the previous part, connect an administrator's wallet to the application and create at least one NFT. The new contract that you deployed in this section has no NFTs to start with. 7. Offer a bottle for sale: 1. Open the application and click **Trading > Sell bottles**. The sale page opens and shows the bottles that you own, as in this picture: ![The Sell bottle page shows the bottles that you can offer for sale](/img/tutorials/nft-marketplace-2-sell.png) 2. Set the price for a bottle and then click **Sell**. 3. Approve the transaction in your wallet and wait for the page to refresh. When the page refreshes, the bottle updates to show "Traded" and the offer price, as in this picture: ![The bottle marked available for sale](/img/tutorials/nft-markeplace-2-traded-bottle.png) ## Add a catalog and sales page In this section, you add a catalog page to show the bottles that are on sale and allow users to buy them. 1. Open the file `app/src/WineCataloguePage.tsx` and replace it with this code: ```typescript import { InfoOutlined } from '@mui/icons-material'; import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; import { Box, Button, Card, CardActions, CardContent, CardHeader, CardMedia, ImageList, Pagination, Tooltip, useMediaQuery, } from '@mui/material'; import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; import BigNumber from 'bignumber.js'; import { useFormik } from 'formik'; import { useSnackbar } from 'notistack'; import React, { Fragment, useState } from 'react'; import * as yup from 'yup'; import { UserContext, UserContextType } from './App'; import ConnectButton from './ConnectWallet'; import { TransactionInvalidBeaconError } from './TransactionInvalidBeaconError'; import { address, nat } from './type-aliases'; const itemPerPage: number = 6; type OfferEntry = [nat, Offer]; type Offer = { owner: address; price: nat; }; const validationSchema = yup.object({}); export default function WineCataloguePage() { const { Tezos, nftContractTokenMetadataMap, setUserAddress, setUserBalance, wallet, userAddress, nftContract, refreshUserContextOnPageReload, storage, } = React.useContext(UserContext) as UserContextType; const [selectedOfferEntry, setSelectedOfferEntry] = React.useState(null); const formik = useFormik({ initialValues: { quantity: 1, }, validationSchema: validationSchema, onSubmit: (values) => { console.log('onSubmit: (values)', values, selectedOfferEntry); buy(selectedOfferEntry!); }, }); const { enqueueSnackbar } = useSnackbar(); const [currentPageIndex, setCurrentPageIndex] = useState(1); const buy = async (selectedOfferEntry: OfferEntry) => { try { const op = await nftContract?.methods .buy( BigNumber(selectedOfferEntry[0]) as nat, selectedOfferEntry[1].owner ) .send({ amount: selectedOfferEntry[1].price.toNumber(), mutez: true, }); await op?.confirmation(2); enqueueSnackbar( 'Bought ' + 1 + ' unit of Wine collection (token_id:' + selectedOfferEntry[0] + ')', { variant: 'success', } ); refreshUserContextOnPageReload(); //force all app to refresh the context } catch (error) { console.table(`Error: ${JSON.stringify(error, null, 2)}`); let tibe: TransactionInvalidBeaconError = new TransactionInvalidBeaconError(error); enqueueSnackbar(tibe.data_message, { variant: 'error', autoHideDuration: 10000, }); } }; const isDesktop = useMediaQuery('(min-width:1100px)'); const isTablet = useMediaQuery('(min-width:600px)'); return ( Wine catalogue {storage?.extension.offers && storage?.extension.offers.size != 0 ? ( setCurrentPageIndex(value)} count={Math.ceil( Array.from(storage?.extension.offers.entries()).length / itemPerPage )} showFirstButton showLastButton /> {Array.from(storage?.extension.offers.entries()) .filter((_, index) => index >= currentPageIndex * itemPerPage - itemPerPage && index < currentPageIndex * itemPerPage ? true : false ) .map(([token_id, offer]) => ( {' '} {'ID : ' + token_id.toString()}{' '} {'Description : ' + nftContractTokenMetadataMap.get( token_id.toString() )?.description} {'Seller : ' + offer.owner}{' '} } > } title={ nftContractTokenMetadataMap.get(token_id.toString()) ?.name } /> {' '} {'Price : ' + offer.price.dividedBy(1000000) + ' XTZ'} {!userAddress ? ( ) : (
{ setSelectedOfferEntry([token_id, offer]); formik.handleSubmit(values); }} >
)}
))}
) : ( There are no NFTs for sale. Mint some and offer them for sale. )}
); } ``` 2. Disconnect your administrator account from the application and connect with a different account that has enough tez to buy a bottle. 3. In the web application, click **Trading > Wine catalogue**. The page looks like this: ![The catalog page shows one bottle for sale](/img/tutorials/nft-marketplace-2-buy.png) 4. Buy a bottle by clicking **Buy** and confirming the transaction in your wallet. 5. When the transaction completes, click **Trading > Sell bottles** and see that you own the bottle and that you can offer it for sale. ## Troubleshooting If the application does not behave as you expect, try these troubleshooting steps: 1. Compare your application to the completed application at https://github.com/trilitech/tutorial-applications/tree/main/nft-marketplace/part-2. 2. Make sure that your address is listed as the administrator in the `contracts/nft.storageList.jsligo` file. 3. Run these commands to redeploy the contract and update the web application with information about it: ``` TAQ_LIGO_IMAGE=ligolang/ligo:1.10.0 taq compile nft.jsligo taq deploy nft.tz -e "testing" taq generate types ./app/src cd app yarn dev ``` 4. Verify that the address of the deployed contract in the `app/.env` file is correct. ## Summary Now you and other users can buy and sell NFTs from the marketplace dApp. In the next part, you create a different type of token, called a single-asset token. Instead of creating multiple token types with a quantity of exactly 1 as with the NFTs in this part, you create a single token type with any quantity you want. For the complete content of the contract and web app at the end of this part, see the completed part 2 app at https://github.com/trilitech/tutorial-applications/tree/main/nft-marketplace/part-2. To continue, go to [Part 3: Managing tokens with quantities](/tutorials/build-an-nft-marketplace/part-3). # Part 3: Managing tokens with quantities Because only one of each NFT can exist, they are not the right token type to represent wine bottles, which have a type and a quantity of bottles of that type. So in this part, you change the application to use a single-asset template, which lets you create a single token ID with a quantity that you define. Of course, a wine store has many different bottles of wine with different quantities, so in the next part, you use a multi-asset template to represent bottles in that situation. You can continue from your code from part 2 or start from the completed version here: https://github.com/trilitech/tutorial-applications/tree/main/nft-marketplace/part-2. If you start from the completed version, run these commands to install dependencies for the web application: ```bash npm i cd ./app yarn install cd .. ``` ## Updating the smart contract To use the single-asset template, you must change the code that your smart contract imports from the NFT template to the single-asset template: 1. In the `contracts/nft.jsligo` file, change the first line to this code: ```jsligo #import "@ligo/fa/lib/fa2/asset/extendable_single_asset.impl.jsligo" "FA2Impl" ``` 2. Change the offer type to store a quantity and a price, as in this code: ```jsligo export type offer = { quantity: nat, price: nat }; ``` 3. Change the **offers** BigMap in the `Extension` field: ```jsligo export type Extension = { administrators: set
, offers: map, //user sells an offer }; ``` Now the **offers** value is indexed on the address of the seller instead of the token ID because there is only one token ID. 4. Replace the `mint` entrypoint with this code: ```jsligo @entry const mint = ( [quantity, name, description, symbol, ipfsUrl]: [ nat, bytes, bytes, bytes, bytes ], s: storage ): ret => { if (quantity <= (0 as nat)) return failwith("0"); if (! Set.mem(Tezos.get_sender(), s.extension.administrators)) return failwith("1"); const token_info: map = Map.literal( list( [ ["name", name], ["description", description], ["interfaces", (bytes `["TZIP-12"]`)], ["artifactUri", ipfsUrl], ["displayUri", ipfsUrl], ["thumbnailUri", ipfsUrl], ["symbol", symbol], ["decimals", (bytes `0`)] ] ) ) as map; return [ list([]) as list, { ...s, ledger: Big_map.literal(list([[Tezos.get_sender(), quantity as nat]])) as FA2Impl.ledger, token_metadata: Big_map.add( 0 as nat, { token_id: 0 as nat, token_info: token_info }, s.token_metadata ), operators: Big_map.empty as FA2Impl.operators, } ] }; ``` This updated entrypoint accepts a parameter for the number of tokens to mint. 5. Replace the `sell` entrypoint with this code: ```jsligo @entry const sell = ([quantity, price]: [nat, nat], s: storage): ret => { //check balance of seller const sellerBalance = FA2Impl.get_amount_for_owner(s)(Tezos.get_source()); if (quantity > sellerBalance) return failwith("2"); //need to allow the contract itself to be an operator on behalf of the seller const newOperators = FA2Impl.add_operator( s.operators, Tezos.get_source(), Tezos.get_self_address() ); //DECISION CHOICE: if offer already exists, we just override it return [ list([]) as list, { ...s, extension: { ...s.extension, offers: Map.add( Tezos.get_source(), { quantity: quantity, price: price }, s.extension.offers ) }, operators: newOperators } ] }; ``` This updated entrypoint accepts a quantity to offer for sale instead of a token ID. It also overrides any existing offers for the token. 6. Replace the `buy` entrypoint with this code: ```jsligo @entry const buy = ([quantity, seller]: [nat, address], s: storage): ret => { //search for the offer return match(Map.find_opt(seller, s.extension.offers)) { when (None()): failwith("3") when (Some(offer)): do { //check if quantity is enough if (quantity > offer.quantity) return failwith("4"); //check if amount have been paid enough if (Tezos.get_amount() < (offer.price * quantity) * (1 as mutez)) return failwith( "5" ); // prepare transfer of XTZ to seller const op = Tezos.transaction( unit, (offer.price * quantity) * (1 as mutez), Tezos.get_contract_with_error(seller, "6") ); //transfer tokens from seller to buyer let ledger = FA2Impl.decrease_token_amount_for_user(s.ledger, seller, quantity); ledger = FA2Impl.increase_token_amount_for_user( ledger, Tezos.get_source(), quantity ); //update new offer const newOffer = { ...offer, quantity: abs(offer.quantity - quantity) }; return [ list([op]) as list, { ...s, extension: { ...s.extension, offers: Map.update(seller, Some(newOffer), s.extension.offers) }, ledger: ledger, } ] } } }; ``` This updated entrypoint accepts the number of tokens to buy, verifies that the quantity is less than or equal to the quantity offered for sale, verifies the sale price, and updates the offer. It allows a buyer to buy the full amount of tokens for sale or fewer than the offered amount. 7. Replace the content of the `contracts/nft.storageList.jsligo` file with this code: ```jsligo #import "nft.jsligo" "Contract" #import "@ligo/fa/lib/fa2/asset/extendable_single_asset.impl.jsligo" "FA2Impl" const default_storage: Contract.storage = { extension: { administrators: Set.literal( list(["tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb" as address]) ) as set
, offers: Map.empty as map, }, ledger: Big_map.empty as FA2Impl.ledger, metadata: Big_map.literal( list( [ ["", bytes `tezos-storage:data`], [ "data", bytes `{ "name":"FA2 NFT Marketplace", "description":"Example of FA2 implementation", "version":"0.0.1", "license":{"name":"MIT"}, "authors":["Marigold"], "homepage":"https://marigold.dev", "source":{ "tools":["Ligo"], "location":"https://github.com/ligolang/contract-catalogue/tree/main/lib/fa2"}, "interfaces":["TZIP-012"], "errors": [], "views": [] }` ] ] ) ) as FA2Impl.TZIP16.metadata, token_metadata: Big_map.empty as FA2Impl.TZIP12.tokenMetadata, operators: Big_map.empty as FA2Impl.operators, }; ``` 8. As in the previous parts, update the administrators to include addresses that you have access to. 9. Compile and deploy the new contract: ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.10.0 taq compile nft.jsligo taq deploy nft.tz -e "testing" ``` ## Updating the frontend 1. Generate the TypeScript classes and start the server: ```bash taq generate types ./app/src cd ./app yarn dev ``` 2. In the file `app/src/App.tsx`, replace the function `refreshUserContextOnPageReload` with this code: ```typescript const refreshUserContextOnPageReload = async () => { console.log("refreshUserContext"); //CONTRACT try { let c = await Tezos.contract.at(nftContractAddress, tzip12); console.log("nftContractAddress", nftContractAddress); let nftContract: NftWalletType = await Tezos.wallet.at( nftContractAddress ); const storage = (await nftContract.storage()) as Storage; try { let tokenMetadata: TZIP21TokenMetadata = (await c .tzip12() .getTokenMetadata(0)) as TZIP21TokenMetadata; nftContractTokenMetadataMap.set("0", tokenMetadata); setnftContractTokenMetadataMap(new Map(nftContractTokenMetadataMap)); //new Map to force refresh } catch (error) { console.log("error refreshing nftContractTokenMetadataMap: "); } setNftContract(nftContract); setStorage(storage); } catch (error) { console.log("error refreshing nft contract: ", error); } //USER const activeAccount = await wallet.client.getActiveAccount(); if (activeAccount) { setUserAddress(activeAccount.address); const balance = await Tezos.tz.getBalance(activeAccount.address); setUserBalance(balance.toNumber()); } console.log("refreshUserContext ended."); }; ``` This update shows information about the single-asset token correctly in the UI. 3. Replace the content of the `app/src/MintPage.tsx` file with this code: ```typescript import OpenWithIcon from "@mui/icons-material/OpenWith"; import { Button, CardHeader, CardMedia, MobileStepper, Stack, SwipeableDrawer, TextField, Toolbar, useMediaQuery, } from "@mui/material"; import Box from "@mui/material/Box"; import Card from "@mui/material/Card"; import CardContent from "@mui/material/CardContent"; import Paper from "@mui/material/Paper"; import Typography from "@mui/material/Typography"; import { BigNumber } from "bignumber.js"; import { useSnackbar } from "notistack"; import React, { useEffect, useState } from "react"; import { TZIP21TokenMetadata, UserContext, UserContextType } from "./App"; import { TransactionInvalidBeaconError } from "./TransactionInvalidBeaconError"; import { AddCircleOutlined, Close, KeyboardArrowLeft, KeyboardArrowRight, } from "@mui/icons-material"; import { char2Bytes } from "@taquito/utils"; import { useFormik } from "formik"; import SwipeableViews from "react-swipeable-views"; import * as yup from "yup"; import { address, bytes, nat } from "./type-aliases"; export default function MintPage() { const { userAddress, storage, nftContract, refreshUserContextOnPageReload, nftContractTokenMetadataMap, } = React.useContext(UserContext) as UserContextType; const { enqueueSnackbar } = useSnackbar(); const [pictureUrl, setPictureUrl] = useState(""); const [file, setFile] = useState(null); const [activeStep, setActiveStep] = React.useState(0); const handleNext = () => { setActiveStep((prevActiveStep) => prevActiveStep + 1); }; const handleBack = () => { setActiveStep((prevActiveStep) => prevActiveStep - 1); }; const handleStepChange = (step: number) => { setActiveStep(step); }; const validationSchema = yup.object({ name: yup.string().required("Name is required"), description: yup.string().required("Description is required"), symbol: yup.string().required("Symbol is required"), quantity: yup .number() .required("Quantity is required") .positive("ERROR: The number must be greater than 0!"), }); const formik = useFormik({ initialValues: { name: "", description: "", token_id: 0, symbol: "WINE", quantity: 1, } as TZIP21TokenMetadata & { quantity: number }, validationSchema: validationSchema, onSubmit: (values) => { mint(values); }, }); //open mint drawer if admin useEffect(() => { if ( storage && storage!.extension.administrators.indexOf(userAddress! as address) < 0 ) setFormOpen(false); else setFormOpen(true); }, [userAddress]); const mint = async ( newTokenDefinition: TZIP21TokenMetadata & { quantity: number } ) => { try { //IPFS if (file) { const formData = new FormData(); formData.append("file", file); const requestHeaders: HeadersInit = new Headers(); requestHeaders.set( "pinata_api_key", `${import.meta.env.VITE_PINATA_API_KEY}` ); requestHeaders.set( "pinata_secret_api_key", `${import.meta.env.VITE_PINATA_API_SECRET}` ); const resFile = await fetch( "https://api.pinata.cloud/pinning/pinFileToIPFS", { method: "post", body: formData, headers: requestHeaders, } ); const responseJson = await resFile.json(); console.log("responseJson", responseJson); const thumbnailUri = `ipfs://${responseJson.IpfsHash}`; setPictureUrl( `https://gateway.pinata.cloud/ipfs/${responseJson.IpfsHash}` ); const op = await nftContract!.methods .mint( new BigNumber(newTokenDefinition.quantity) as nat, char2Bytes(newTokenDefinition.name!) as bytes, char2Bytes(newTokenDefinition.description!) as bytes, char2Bytes(newTokenDefinition.symbol!) as bytes, char2Bytes(thumbnailUri) as bytes ) .send(); //close directly the form setFormOpen(false); enqueueSnackbar( "Wine collection is minting ... it will be ready on next block, wait for the confirmation message before minting another collection", { variant: "info" } ); await op.confirmation(2); enqueueSnackbar("Wine collection minted", { variant: "success" }); refreshUserContextOnPageReload(); //force all app to refresh the context } } catch (error) { console.table(`Error: ${JSON.stringify(error, null, 2)}`); let tibe: TransactionInvalidBeaconError = new TransactionInvalidBeaconError(error); enqueueSnackbar(tibe.data_message, { variant: "error", autoHideDuration: 10000, }); } }; const [formOpen, setFormOpen] = useState(false); const toggleDrawer = (open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => { if ( event.type === "keydown" && ((event as React.KeyboardEvent).key === "Tab" || (event as React.KeyboardEvent).key === "Shift") ) { return; } setFormOpen(open); }; const isTablet = useMediaQuery("(min-width:600px)"); return ( {storage ? ( ) : ( "" )}
Mint a new collection {pictureUrl ? ( ) : ( "" )}
Mint your wine collection {nftContractTokenMetadataMap.size != 0 ? ( {Array.from(nftContractTokenMetadataMap!.entries()).map( ([token_id, token]) => ( {"ID : " + token_id} {"Symbol : " + token.symbol} {"Description : " + token.description} ) )} Next } backButton={ } /> ) : ( No NFTs yet. Click "Connect wallet" and mint an NFT. )}
); } ``` This update changes the mint function to add a quantity field and remove the token ID field. 4. Replace the content of the `app/src/OffersPage.tsx` file with this code: ```typescript import { InfoOutlined } from "@mui/icons-material"; import SellIcon from "@mui/icons-material/Sell"; import * as api from "@tzkt/sdk-api"; import { Box, Button, Card, CardActions, CardContent, CardHeader, CardMedia, ImageList, InputAdornment, Pagination, TextField, Tooltip, Typography, useMediaQuery, } from "@mui/material"; import Paper from "@mui/material/Paper"; import BigNumber from "bignumber.js"; import { useFormik } from "formik"; import { useSnackbar } from "notistack"; import React, { Fragment, useEffect, useState } from "react"; import * as yup from "yup"; import { UserContext, UserContextType } from "./App"; import ConnectButton from "./ConnectWallet"; import { TransactionInvalidBeaconError } from "./TransactionInvalidBeaconError"; import { address, nat } from "./type-aliases"; const itemPerPage: number = 6; const validationSchema = yup.object({ price: yup .number() .required("Price is required") .positive("ERROR: The number must be greater than 0!"), quantity: yup .number() .required("Quantity is required") .positive("ERROR: The number must be greater than 0!"), }); type Offer = { price: nat; quantity: nat; }; export default function OffersPage() { api.defaults.baseUrl = "https://api.shadownet.tzkt.io"; const [selectedTokenId, setSelectedTokenId] = React.useState(0); const [currentPageIndex, setCurrentPageIndex] = useState(1); let [ownerOffers, setOwnerOffers] = React.useState(null); let [ownerBalance, setOwnerBalance] = React.useState(0); const { nftContract, nftContractTokenMetadataMap, userAddress, storage, refreshUserContextOnPageReload, Tezos, setUserAddress, setUserBalance, wallet, } = React.useContext(UserContext) as UserContextType; const { enqueueSnackbar } = useSnackbar(); const formik = useFormik({ initialValues: { price: 0, quantity: 1, }, validationSchema: validationSchema, onSubmit: (values) => { console.log("onSubmit: (values)", values, selectedTokenId); sell(selectedTokenId, values.quantity, values.price); }, }); const initPage = async () => { if (storage) { console.log("context is not empty, init page now"); const ledgerBigMapId = ( storage.ledger as unknown as { id: BigNumber } ).id.toNumber(); const ownersKeys = await api.bigMapsGetKeys(ledgerBigMapId, { micheline: "Json", active: true, }); await Promise.all( ownersKeys.map(async (ownerKey) => { if (ownerKey.key === userAddress) { const ownerBalance = await storage.ledger.get( userAddress as address ); setOwnerBalance(ownerBalance.toNumber()); const ownerOffers = await storage.extension.offers.get( userAddress as address ); if (ownerOffers && ownerOffers.quantity != BigNumber(0)) setOwnerOffers(ownerOffers!); console.log( "found for " + ownerKey.key + " on token_id " + 0 + " with balance " + ownerBalance ); } else { console.log("skip to next owner"); } }) ); } else { console.log("context is empty, wait for parent and retry ..."); } }; useEffect(() => { (async () => { console.log("after a storage changed"); await initPage(); })(); }, [storage]); useEffect(() => { (async () => { console.log("on Page init"); await initPage(); })(); }, []); const sell = async (token_id: number, quantity: number, price: number) => { try { const op = await nftContract?.methods .sell( BigNumber(quantity) as nat, BigNumber(price * 1000000) as nat //to mutez ) .send(); await op?.confirmation(2); enqueueSnackbar( "Wine collection (token_id=" + token_id + ") offer for " + quantity + " units at price of " + price + " XTZ", { variant: "success" } ); refreshUserContextOnPageReload(); //force all app to refresh the context } catch (error) { console.table(`Error: ${JSON.stringify(error, null, 2)}`); let tibe: TransactionInvalidBeaconError = new TransactionInvalidBeaconError(error); enqueueSnackbar(tibe.data_message, { variant: "error", autoHideDuration: 10000, }); } }; const isDesktop = useMediaQuery("(min-width:1100px)"); const isTablet = useMediaQuery("(min-width:600px)"); return ( Sell my bottles {ownerBalance != 0 ? ( setCurrentPageIndex(value)} count={Math.ceil(1 / itemPerPage)} showFirstButton showLastButton /> {"ID : " + 0} {"Description : " + nftContractTokenMetadataMap.get("0")?.description} } > } title={nftContractTokenMetadataMap.get("0")?.name} /> {"Owned : " + ownerBalance} {ownerOffers ? "Traded : " + ownerOffers?.quantity + " (price : " + ownerOffers?.price.dividedBy(1000000) + " Tz/b)" : ""} {!userAddress ? ( ) : (
{ setSelectedTokenId(0); formik.handleSubmit(values); }} > ), }} />
)}
) : ( Sorry, you don't own any bottles, buy or mint some first )}
); } ``` This update changes the offers page to allow owners to specify the number of tokens to offer for sale. 5. Replace the content of the `app/src/WineCataloguePage.tsx` with this code: ```typescript import { InfoOutlined } from "@mui/icons-material"; import ShoppingCartIcon from "@mui/icons-material/ShoppingCart"; import { Button, Card, CardActions, CardContent, CardHeader, CardMedia, ImageList, InputAdornment, Pagination, TextField, Tooltip, Typography, useMediaQuery, } from "@mui/material"; import Box from "@mui/material/Box"; import Paper from "@mui/material/Paper"; import BigNumber from "bignumber.js"; import { useFormik } from "formik"; import { useSnackbar } from "notistack"; import React, { Fragment, useState } from "react"; import * as yup from "yup"; import { UserContext, UserContextType } from "./App"; import ConnectButton from "./ConnectWallet"; import { TransactionInvalidBeaconError } from "./TransactionInvalidBeaconError"; import { address, nat } from "./type-aliases"; const itemPerPage: number = 6; type OfferEntry = [address, Offer]; type Offer = { price: nat; quantity: nat; }; const validationSchema = yup.object({ quantity: yup .number() .required("Quantity is required") .positive("ERROR: The number must be greater than 0!"), }); export default function WineCataloguePage() { const { Tezos, nftContractTokenMetadataMap, setUserAddress, setUserBalance, wallet, userAddress, nftContract, refreshUserContextOnPageReload, storage, } = React.useContext(UserContext) as UserContextType; const [selectedOfferEntry, setSelectedOfferEntry] = React.useState(null); const formik = useFormik({ initialValues: { quantity: 1, }, validationSchema: validationSchema, onSubmit: (values) => { console.log("onSubmit: (values)", values, selectedOfferEntry); buy(values.quantity, selectedOfferEntry!); }, }); const { enqueueSnackbar } = useSnackbar(); const [currentPageIndex, setCurrentPageIndex] = useState(1); const buy = async (quantity: number, selectedOfferEntry: OfferEntry) => { try { const op = await nftContract?.methods .buy(BigNumber(quantity) as nat, selectedOfferEntry[0]) .send({ amount: quantity * selectedOfferEntry[1].price.toNumber(), mutez: true, }); await op?.confirmation(2); enqueueSnackbar( "Bought " + quantity + " unit of Wine collection (token_id:" + selectedOfferEntry[0][1] + ")", { variant: "success", } ); refreshUserContextOnPageReload(); //force all app to refresh the context } catch (error) { console.table(`Error: ${JSON.stringify(error, null, 2)}`); let tibe: TransactionInvalidBeaconError = new TransactionInvalidBeaconError(error); enqueueSnackbar(tibe.data_message, { variant: "error", autoHideDuration: 10000, }); } }; const isDesktop = useMediaQuery("(min-width:1100px)"); const isTablet = useMediaQuery("(min-width:600px)"); return ( Wine catalogue {storage?.extension.offers && storage?.extension.offers.size != 0 ? ( setCurrentPageIndex(value)} count={Math.ceil( Array.from(storage?.extension.offers.entries()).filter( ([_, offer]) => offer.quantity.isGreaterThan(0) ).length / itemPerPage )} showFirstButton showLastButton /> {Array.from(storage?.extension.offers.entries()) .filter(([_, offer]) => offer.quantity.isGreaterThan(0)) .filter((_, index) => index >= currentPageIndex * itemPerPage - itemPerPage && index < currentPageIndex * itemPerPage ? true : false ) .map(([owner, offer]) => ( {"ID : " + 0} {"Description : " + nftContractTokenMetadataMap.get("0") ?.description} {"Seller : " + owner} } > } title={nftContractTokenMetadataMap.get("0")?.name} /> {"Price : " + offer.price.dividedBy(1000000) + " XTZ/bottle"} {"Available units : " + offer.quantity} {!userAddress ? ( ) : (
{ setSelectedOfferEntry([owner, offer]); formik.handleSubmit(values); }} > ), }} /> )}
))}
) : ( There are no NFTs for sale. Mint some and offer them for sale. )}
); } ``` Like the other files, this update removes the token ID and adds the quantity field. 6. Restart the frontend application. 7. As you did in the previous part, connect the administrator's wallet to the application. 8. Create a token and specify a quantity to mint. For example, you can use this information: * `name`: Saint Emilion - Franc la Rose * `symbol`: SEMIL * `description`: Grand cru 2007 * `quantity`: 1000 ![The minting page, showing the creation of 1000 tokens](/img/tutorials/nft-marketplace-3-minting.png) When you approve the transaction in your wallet and the transaction completes, the page refreshes automatically and shows the new token. 9. Click **Trading > Sell bottles**, set the quantity to offer and the price per token, as shown in this picture: ![Setting the price and quantity for the offer](/img/tutorials/nft-marketplace-3-offer.png) 10. Click **Sell** and approve the transaction in your wallet. 11. When the transaction completes, connect with a different account, click **Trading > Wine catalogue**, and buy some bottles of wine, as shown in this picture: ![Buying bottles of wine](/img/tutorials/nft-marketplace-3-buy.png) 12. When the transaction completes, you can see that the different account owns the tokens and can offer them for sale for a different price. You can also get the address of the contract from the `.taq/config.local.testing.json` file or `app/.env` file and look up the contract in a block explorer. Because the contract is still FA2 compliant, the block explorer shows the token holders and the quantity of the tokens they have, such as in this picture from the [tzkt.io](https://shadownet.tzkt.io/) block explorer: ![The block explorer, showing the accounts that own the token](/img/tutorials/nft-marketplace-3-token-holders.png) ## Summary Now you can manage tokens that have a quantity, but the app can manage only one type of token. For the complete content of the contract and web app at the end of this part, see the completed part 3 app at https://github.com/trilitech/tutorial-applications/tree/main/nft-marketplace/part-3. In the next part, you update the application to create a multi-asset contract that can handle multiple types of tokens with different quantities. To continue, go to [Part 4: Handling multi-asset tokens](/tutorials/build-an-nft-marketplace/part-4). # Part 4: Handling multi-asset tokens Because a wine store can have many bottles of many different types, the appropriate template to use is the multi-asset template. With this template, you can create as many token types as you need and set a different quantity for each type. You can continue from your code from part 3 or start from the completed version here: https://github.com/trilitech/tutorial-applications/tree/main/nft-marketplace/part-3. If you start from the completed version, run these commands to install dependencies for the web application: ```bash npm i cd ./app yarn install cd .. ``` ## Updating the smart contract To use the multi-asset template, you must change the code that your smart contract imports from the NFT template to the multi-asset template: 1. In the `contracts/nft.jsligo` file, change the first line to this code: ```jsligo #import "@ligo/fa/lib/fa2/asset/extendable_multi_asset.impl.jsligo" "FA2Impl" ``` 2. In the storage, change the `offers` value to `map<[address, nat], offer>`. The storage type looks like this: ```jsligo export type Extension = { administrators: set
, offers: map<[address, nat], offer>, //user sells an offer for a token_id }; ``` Now the `offers` map is indexed on the address of the seller and the ID of the token for sale. 3. Replace the `mint` entrypoint with this code: ```jsligo @entry const mint = ( [token_id, quantity, name, description, symbol, ipfsUrl]: [ nat, nat, bytes, bytes, bytes, bytes ], s: storage ): ret => { if (quantity <= (0 as nat)) return failwith("0"); if (! Set.mem(Tezos.get_sender(), s.extension.administrators)) return failwith( "1" ); const token_info: map = Map.literal( list( [ ["name", name], ["description", description], ["interfaces", (bytes `["TZIP-12"]`)], ["artifactUri", ipfsUrl], ["displayUri", ipfsUrl], ["thumbnailUri", ipfsUrl], ["symbol", symbol], ["decimals", (bytes `0`)] ] ) ) as map; return [ list([]) as list, { ...s, ledger: Big_map.add( [Tezos.get_sender(), token_id], quantity as nat, s.ledger ) as FA2Impl.ledger, token_metadata: Big_map.add( token_id, { token_id: token_id, token_info: token_info }, s.token_metadata ), operators: Big_map.empty as FA2Impl.operators } ] }; ``` This updated mint entrypoint accepts both a token ID and a quantity and mints the specified number of that token. 4. Replace the `sell` entrypoint with this code: ```jsligo @entry const sell = ([token_id, quantity, price]: [nat, nat, nat], s: storage): ret => { //check balance of seller const sellerBalance = FA2Impl.get_for_user([s.ledger, Tezos.get_source(), token_id]); if (quantity > sellerBalance) return failwith("2"); //need to allow the contract itself to be an operator on behalf of the seller const newOperators = FA2Impl.add_operator( [s.operators, Tezos.get_source(), Tezos.get_self_address(), token_id] ); //DECISION CHOICE: if offer already exists, we just override it return [ list([]) as list, { ...s, extension: { ...s.extension, offers: Map.add( [Tezos.get_source(), token_id], { quantity: quantity, price: price }, s.extension.offers ) }, operators: newOperators } ] }; ``` Like the mint entrypoint, this entrypoint now accepts a token ID and quantity as parameters. 5. Replace the `buy` entrypoint with this code: ```jsligo @entry const buy = ([token_id, quantity, seller]: [nat, nat, address], s: storage): ret => { //search for the offer return match(Map.find_opt([seller, token_id], s.extension.offers)) { when (None()): failwith("3") when (Some(offer)): do { //check if amount have been paid enough if (Tezos.get_amount() < offer.price * (1 as mutez)) return failwith( "5" ); // prepare transfer of XTZ to seller const op = Tezos.transaction( unit, offer.price * (1 as mutez), Tezos.get_contract_with_error(seller, "6") ); //transfer tokens from seller to buyer let ledger = FA2Impl.decrease_token_amount_for_user( [s.ledger, seller, token_id, quantity] ); ledger = FA2Impl.increase_token_amount_for_user( [ledger, Tezos.get_source(), token_id, quantity] ); //update new offer const newOffer = { ...offer, quantity: abs(offer.quantity - quantity) }; return [ list([op]) as list, { ...s, extension: { ...s.extension, offers: Map.update( [seller, token_id], Some(newOffer), s.extension.offers ) }, ledger: ledger } ] } } }; ``` 6. Update the `contracts/nft.storageList.jsligo` with this code: ```jsligo #import "nft.jsligo" "Contract" #import "@ligo/fa/lib/fa2/asset/extendable_multi_asset.impl.jsligo" "FA2Impl" const default_storage: Contract.storage = { extension: { administrators: Set.literal( list(["tz1QCVQinE8iVj1H2fckqx6oiM85CNJSK9Sx" as address]) ) as set
, offers: Map.empty as map<[address, nat], Contract.offer> }, ledger: Big_map.empty as FA2Impl.ledger, metadata: Big_map.literal( list( [ ["", bytes `tezos-storage:data`], [ "data", bytes `{ "name":"FA2 NFT Marketplace", "description":"Example of FA2 implementation", "version":"0.0.1", "license":{"name":"MIT"}, "authors":["Marigold"], "homepage":"https://marigold.dev", "source":{ "tools":["Ligo"], "location":"https://github.com/ligolang/contract-catalogue/tree/main/lib/fa2"}, "interfaces":["TZIP-012"], "errors": [], "views": [] }` ] ] ) ) as FA2Impl.TZIP16.metadata, token_metadata: Big_map.empty as FA2Impl.TZIP12.tokenMetadata, operators: Big_map.empty as FA2Impl.operators, }; ``` 7. As in the previous parts, update the administrators to include addresses that you have access to. 8. Compile and deploy the new contract: ```bash TAQ_LIGO_IMAGE=ligolang/ligo:1.10.0 taq compile nft.jsligo taq deploy nft.tz -e "testing" ``` ## Updating the frontend Now that the contract handles both token IDs and quantities, you must update the frontend in the same way: 1. Generate the TypeScript classes and start the server: ```bash taq generate types ./app/src cd ./app yarn dev ``` 2. In the file `app/src/App.tsx`, replace the function `refreshUserContextOnPageReload` with this code: ```typescript const refreshUserContextOnPageReload = async () => { console.log('refreshUserContext'); //CONTRACT try { let c = await Tezos.contract.at(nftContractAddress, tzip12); console.log('nftContractAddress', nftContractAddress); let nftContract: NftWalletType = await Tezos.wallet.at( nftContractAddress ); const storage = (await nftContract.storage()) as Storage; const token_metadataBigMapId = ( storage.token_metadata as unknown as { id: BigNumber } ).id.toNumber(); const token_ids = await api.bigMapsGetKeys(token_metadataBigMapId, { micheline: 'Json', active: true, }); await Promise.all( token_ids.map(async (token_idKey) => { const key: string = token_idKey.key; let tokenMetadata: TZIP21TokenMetadata = (await c .tzip12() .getTokenMetadata(Number(key))) as TZIP21TokenMetadata; nftContractTokenMetadataMap.set(key, tokenMetadata); }) ); setnftContractTokenMetadataMap(new Map(nftContractTokenMetadataMap)); //new Map to force refresh setNftContract(nftContract); setStorage(storage); } catch (error) { console.log('error refreshing nft contract: ', error); } //USER const activeAccount = await wallet.client.getActiveAccount(); if (activeAccount) { setUserAddress(activeAccount.address); const balance = await Tezos.tz.getBalance(activeAccount.address); setUserBalance(balance.toNumber()); } console.log('refreshUserContext ended.'); }; ``` This function now retrieves all of the tokens in the contract. 3. Replace the content of the `app/src/MintPage.tsx` file with this code: ```typescript import { AddCircleOutlined, Close, KeyboardArrowLeft, KeyboardArrowRight, } from '@mui/icons-material'; import OpenWithIcon from '@mui/icons-material/OpenWith'; import { Box, Button, CardHeader, CardMedia, MobileStepper, Stack, SwipeableDrawer, TextField, Toolbar, useMediaQuery, } from '@mui/material'; import Card from '@mui/material/Card'; import CardContent from '@mui/material/CardContent'; import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; import { char2Bytes } from '@taquito/utils'; import { BigNumber } from 'bignumber.js'; import { useFormik } from 'formik'; import { useSnackbar } from 'notistack'; import React, { useEffect, useState } from 'react'; import SwipeableViews from 'react-swipeable-views'; import * as yup from 'yup'; import { TZIP21TokenMetadata, UserContext, UserContextType } from './App'; import { TransactionInvalidBeaconError } from './TransactionInvalidBeaconError'; import { address, bytes, nat } from './type-aliases'; export default function MintPage() { const { userAddress, nftContract, refreshUserContextOnPageReload, nftContractTokenMetadataMap, storage, } = React.useContext(UserContext) as UserContextType; const { enqueueSnackbar } = useSnackbar(); const [pictureUrl, setPictureUrl] = useState(''); const [file, setFile] = useState(null); const [activeStep, setActiveStep] = React.useState(0); const handleNext = () => { setActiveStep((prevActiveStep) => prevActiveStep + 1); }; const handleBack = () => { setActiveStep((prevActiveStep) => prevActiveStep - 1); }; const handleStepChange = (step: number) => { setActiveStep(step); }; const validationSchema = yup.object({ name: yup.string().required('Name is required'), description: yup.string().required('Description is required'), symbol: yup.string().required('Symbol is required'), quantity: yup .number() .required('Quantity is required') .positive('ERROR: The number must be greater than 0!'), }); const formik = useFormik({ initialValues: { name: '', description: '', token_id: 0, symbol: 'WINE', quantity: 1, } as TZIP21TokenMetadata & { quantity: number }, validationSchema: validationSchema, onSubmit: (values) => { mint(values); }, }); //open mint drawer if admin useEffect(() => { if ( storage && storage!.extension.administrators.indexOf(userAddress! as address) < 0 ) setFormOpen(false); else setFormOpen(true); }, [userAddress]); useEffect(() => { (async () => { if ( nftContractTokenMetadataMap && nftContractTokenMetadataMap.size > 0 ) { formik.setFieldValue('token_id', nftContractTokenMetadataMap.size); } })(); }, [nftContractTokenMetadataMap?.size]); const mint = async ( newTokenDefinition: TZIP21TokenMetadata & { quantity: number } ) => { try { //IPFS if (file) { const formData = new FormData(); formData.append('file', file); const requestHeaders: HeadersInit = new Headers(); requestHeaders.set( 'pinata_api_key', `${import.meta.env.VITE_PINATA_API_KEY}` ); requestHeaders.set( 'pinata_secret_api_key', `${import.meta.env.VITE_PINATA_API_SECRET}` ); const resFile = await fetch( 'https://api.pinata.cloud/pinning/pinFileToIPFS', { method: 'post', body: formData, headers: requestHeaders, } ); const responseJson = await resFile.json(); console.log('responseJson', responseJson); const thumbnailUri = `ipfs://${responseJson.IpfsHash}`; setPictureUrl( `https://gateway.pinata.cloud/ipfs/${responseJson.IpfsHash}` ); const op = await nftContract!.methods .mint( new BigNumber(newTokenDefinition.token_id) as nat, new BigNumber(newTokenDefinition.quantity) as nat, char2Bytes(newTokenDefinition.name!) as bytes, char2Bytes(newTokenDefinition.description!) as bytes, char2Bytes(newTokenDefinition.symbol!) as bytes, char2Bytes(thumbnailUri) as bytes ) .send(); //close directly the form setFormOpen(false); enqueueSnackbar( 'Wine collection is minting ... it will be ready on next block, wait for the confirmation message before minting another collection', { variant: 'info' } ); await op.confirmation(2); enqueueSnackbar('Wine collection minted', { variant: 'success' }); refreshUserContextOnPageReload(); //force all app to refresh the context } } catch (error) { console.table(`Error: ${JSON.stringify(error, null, 2)}`); let tibe: TransactionInvalidBeaconError = new TransactionInvalidBeaconError(error); enqueueSnackbar(tibe.data_message, { variant: 'error', autoHideDuration: 10000, }); } }; const [formOpen, setFormOpen] = useState(false); const toggleDrawer = (open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => { if ( event.type === 'keydown' && ((event as React.KeyboardEvent).key === 'Tab' || (event as React.KeyboardEvent).key === 'Shift') ) { return; } setFormOpen(open); }; const isTablet = useMediaQuery('(min-width:600px)'); return ( {storage ? ( ) : ( '' )}
Mint a new collection {pictureUrl ? ( ) : ( '' )}
Mint your wine collection {nftContractTokenMetadataMap.size != 0 ? ( {Array.from(nftContractTokenMetadataMap!.entries()).map( ([token_id, token]) => ( {'ID : ' + token_id} {'Symbol : ' + token.symbol} {'Description : ' + token.description} ) )} Next } backButton={ } /> ) : ( No NFTs yet. Click "Connect wallet" and mint an NFT. )}
); } ``` 4. Replace the content of the `app/src/OffersPage.tsx` file with this code: ```typescript import { InfoOutlined } from '@mui/icons-material'; import SellIcon from '@mui/icons-material/Sell'; import * as api from '@tzkt/sdk-api'; import { Box, Button, Card, CardActions, CardContent, CardHeader, CardMedia, ImageList, InputAdornment, Pagination, TextField, Tooltip, Typography, useMediaQuery, } from '@mui/material'; import Paper from '@mui/material/Paper'; import BigNumber from 'bignumber.js'; import { useFormik } from 'formik'; import { useSnackbar } from 'notistack'; import React, { Fragment, useEffect, useState } from 'react'; import * as yup from 'yup'; import { UserContext, UserContextType } from './App'; import ConnectButton from './ConnectWallet'; import { TransactionInvalidBeaconError } from './TransactionInvalidBeaconError'; import { address, nat } from './type-aliases'; const itemPerPage: number = 6; const validationSchema = yup.object({ price: yup .number() .required('Price is required') .positive('ERROR: The number must be greater than 0!'), quantity: yup .number() .required('Quantity is required') .positive('ERROR: The number must be greater than 0!'), }); type Offer = { price: nat; quantity: nat; }; export default function OffersPage() { api.defaults.baseUrl = 'https://api.shadownet.tzkt.io'; const [selectedTokenId, setSelectedTokenId] = React.useState(0); const [currentPageIndex, setCurrentPageIndex] = useState(1); let [offersTokenIDMap, setOffersTokenIDMap] = React.useState< Map >(new Map()); let [ledgerTokenIDMap, setLedgerTokenIDMap] = React.useState< Map >(new Map()); const { nftContract, nftContractTokenMetadataMap, userAddress, storage, refreshUserContextOnPageReload, Tezos, setUserAddress, setUserBalance, wallet, } = React.useContext(UserContext) as UserContextType; const { enqueueSnackbar } = useSnackbar(); const formik = useFormik({ initialValues: { price: 0, quantity: 1, }, validationSchema: validationSchema, onSubmit: (values) => { console.log('onSubmit: (values)', values, selectedTokenId); sell(selectedTokenId, values.quantity, values.price); }, }); const initPage = async () => { if (storage) { console.log('context is not empty, init page now'); ledgerTokenIDMap = new Map(); offersTokenIDMap = new Map(); const ledgerBigMapId = ( storage.ledger as unknown as { id: BigNumber } ).id.toNumber(); const owner_token_ids = await api.bigMapsGetKeys(ledgerBigMapId, { micheline: 'Json', active: true, }); await Promise.all( owner_token_ids.map(async (owner_token_idKey) => { const key: { address: string; nat: string } = owner_token_idKey.key; if (key.address === userAddress) { const ownerBalance = await storage.ledger.get({ 0: userAddress as address, 1: BigNumber(key.nat) as nat, }); if (ownerBalance.toNumber() !== 0) ledgerTokenIDMap.set(Number(key.nat), ownerBalance); const ownerOffers = await storage.extension.offers.get({ 0: userAddress as address, 1: BigNumber(key.nat) as nat, }); if (ownerOffers && ownerOffers.quantity.toNumber() !== 0) offersTokenIDMap.set(Number(key.nat), ownerOffers); console.log( 'found for ' + key.address + ' on token_id ' + key.nat + ' with balance ' + ownerBalance ); } else { console.log('skip to next owner'); } }) ); setLedgerTokenIDMap(new Map(ledgerTokenIDMap)); //force refresh setOffersTokenIDMap(new Map(offersTokenIDMap)); //force refresh console.log('ledgerTokenIDMap', ledgerTokenIDMap); } else { console.log('context is empty, wait for parent and retry ...'); } }; useEffect(() => { (async () => { console.log('after a storage changed'); await initPage(); })(); }, [storage]); useEffect(() => { (async () => { console.log('on Page init'); await initPage(); })(); }, []); const sell = async (token_id: number, quantity: number, price: number) => { try { const op = await nftContract?.methods .sell( BigNumber(token_id) as nat, BigNumber(quantity) as nat, BigNumber(price * 1000000) as nat //to mutez ) .send(); await op?.confirmation(2); enqueueSnackbar( 'Wine collection (token_id=' + token_id + ') offer for ' + quantity + ' units at price of ' + price + ' XTZ', { variant: 'success' } ); refreshUserContextOnPageReload(); //force all app to refresh the context } catch (error) { console.table(`Error: ${JSON.stringify(error, null, 2)}`); let tibe: TransactionInvalidBeaconError = new TransactionInvalidBeaconError(error); enqueueSnackbar(tibe.data_message, { variant: 'error', autoHideDuration: 10000, }); } }; const isDesktop = useMediaQuery('(min-width:1100px)'); const isTablet = useMediaQuery('(min-width:600px)'); return ( Sell my bottles {ledgerTokenIDMap && ledgerTokenIDMap.size != 0 ? ( setCurrentPageIndex(value)} count={Math.ceil( Array.from(ledgerTokenIDMap.entries()).length / itemPerPage )} showFirstButton showLastButton /> {Array.from(ledgerTokenIDMap.entries()) .filter((_, index) => index >= currentPageIndex * itemPerPage - itemPerPage && index < currentPageIndex * itemPerPage ? true : false ) .map(([token_id, balance]) => ( {' '} {'ID : ' + token_id.toString()}{' '} {'Description : ' + nftContractTokenMetadataMap.get( token_id.toString() )?.description} } > } title={ nftContractTokenMetadataMap.get(token_id.toString()) ?.name } /> {'Owned : ' + balance.toNumber()} {offersTokenIDMap.get(token_id) ? 'Traded : ' + offersTokenIDMap.get(token_id)?.quantity + ' (price : ' + offersTokenIDMap .get(token_id) ?.price.dividedBy(1000000) + ' Tz/b)' : ''} {!userAddress ? ( ) : (
{ setSelectedTokenId(token_id); formik.handleSubmit(values); }} > ), }} />
)}
))}{' '}
) : ( Sorry, you don't own any bottles, buy or mint some first )}
); } ``` 5. Replace the content of the `app/src/WineCataloguePage.tsx` file with this code: ```typescript import { InfoOutlined } from '@mui/icons-material'; import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; import { Box, Button, Card, CardActions, CardContent, CardHeader, CardMedia, ImageList, InputAdornment, Pagination, TextField, Tooltip, useMediaQuery, } from '@mui/material'; import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; import BigNumber from 'bignumber.js'; import { useFormik } from 'formik'; import { useSnackbar } from 'notistack'; import React, { Fragment, useState } from 'react'; import * as yup from 'yup'; import { UserContext, UserContextType } from './App'; import ConnectButton from './ConnectWallet'; import { TransactionInvalidBeaconError } from './TransactionInvalidBeaconError'; import { address, nat } from './type-aliases'; const itemPerPage: number = 6; type OfferEntry = [{ 0: address; 1: nat }, Offer]; type Offer = { price: nat; quantity: nat; }; const validationSchema = yup.object({ quantity: yup .number() .required('Quantity is required') .positive('ERROR: The number must be greater than 0!'), }); export default function WineCataloguePage() { const { Tezos, nftContractTokenMetadataMap, setUserAddress, setUserBalance, wallet, userAddress, nftContract, refreshUserContextOnPageReload, storage, } = React.useContext(UserContext) as UserContextType; const [selectedOfferEntry, setSelectedOfferEntry] = React.useState(null); const formik = useFormik({ initialValues: { quantity: 1, }, validationSchema: validationSchema, onSubmit: (values) => { console.log('onSubmit: (values)', values, selectedOfferEntry); buy(values.quantity, selectedOfferEntry!); }, }); const { enqueueSnackbar } = useSnackbar(); const [currentPageIndex, setCurrentPageIndex] = useState(1); const buy = async (quantity: number, selectedOfferEntry: OfferEntry) => { try { const op = await nftContract?.methods .buy( selectedOfferEntry[0][1], BigNumber(quantity) as nat, selectedOfferEntry[0][0] ) .send({ amount: quantity * selectedOfferEntry[1].price.toNumber(), mutez: true, }); await op?.confirmation(2); enqueueSnackbar( 'Bought ' + quantity + ' unit of Wine collection (token_id:' + selectedOfferEntry[0][1] + ')', { variant: 'success', } ); refreshUserContextOnPageReload(); //force all app to refresh the context } catch (error) { console.table(`Error: ${JSON.stringify(error, null, 2)}`); let tibe: TransactionInvalidBeaconError = new TransactionInvalidBeaconError(error); enqueueSnackbar(tibe.data_message, { variant: 'error', autoHideDuration: 10000, }); } }; const isDesktop = useMediaQuery('(min-width:1100px)'); const isTablet = useMediaQuery('(min-width:600px)'); return ( Wine catalogue {storage?.extension.offers && storage?.extension.offers.size != 0 ? ( setCurrentPageIndex(value)} count={Math.ceil( Array.from(storage?.extension.offers.entries()).filter( ([_, offer]) => offer.quantity.isGreaterThan(0) ).length / itemPerPage )} showFirstButton showLastButton /> {Array.from(storage?.extension.offers.entries()) .filter(([_, offer]) => offer.quantity.isGreaterThan(0)) .filter((_, index) => index >= currentPageIndex * itemPerPage - itemPerPage && index < currentPageIndex * itemPerPage ? true : false ) .map(([key, offer]) => ( {' '} {'ID : ' + key[1].toString()}{' '} {'Description : ' + nftContractTokenMetadataMap.get( key[1].toString() )?.description} {'Seller : ' + key[0]} } > } title={ nftContractTokenMetadataMap.get(key[1].toString())?.name } /> {' '} {'Price : ' + offer.price.dividedBy(1000000) + ' XTZ/bottle'} {'Available units : ' + offer.quantity} {!userAddress ? ( ) : (
{ setSelectedOfferEntry([key, offer]); formik.handleSubmit(values); }} > ), }} /> )}
))}
) : ( There are no NFTs for sale. Mint some and offer them for sale. )}
); } ``` ## Working with the completed application Now you can create, buy, and sell bottles of wine as in the applications in the previous parts. For example, if you connect an administrator account you can create types of wine with quantities and offer them for sale. Then you can connect a different account and buy bottles from the available different types, as in this picture: ![Buying bottles from the available different types](/img/tutorials/nft-marketplace-4-buy.png) ## Summary Now you can create token collections from the different templates that are available in the `@ligo/fa` library, including NFTs, single-asset tokens, and multi-asset tokens. You can create web applications that manage token transactions and show information about tokens. For the complete content of the contract and web app at the end of the tutorial, see the completed part 4 app at https://github.com/trilitech/tutorial-applications/tree/main/nft-marketplace/part-4. If you want to continue with the application, you can extend the contract or application. Here are some ideas: * Create an online marketplace for a different kind of token, like flowers, candy, or cars * Change how tokens behave, like sending a royalty to the marketplace as a sales fee when they are transferred * Add error checking for the application to prevent it from sending invalid transactions * Add new features to the marketplace, such as a shopping cart that lets people buy more than one kind of bottle at a time # Staking and baking with native multisig accounts Estimated time: 2 hours The Seoul protocol introduced native [multisig user accounts](https://octez.tezos.com/docs/alpha/native_multisig.html) to address two common limitations of user accounts: * The entire security of the account usually relies on a single secret key. If this key is compromised, an attacker can gain full control of the account. * The model is not well-suited for organizations or shared accounts, because it does not allow multiple users to manage the account collectively. The usual solution to turn around these common limitations is to use multisig contracts, which are supported in Tezos by a [built-in multisig contract](https://octez.tezos.com/docs/user/multisig.html) or user-created multisig contracts. However, smart contracts cannot stake Tez and they cannot become a baker. Native multisig accounts allow groups of users to collectively stake funds or bake (including participating in governance, managing funds, and staking and unstaking). They also allow a single user to increase the security of their accounts by spreading authority over multiple keys. These native multisig accounts rely on on BLS keys (tz4 addresses), which have been extended with new features. Multisig user accounts support two different multi-signature schemes: * Signature aggregation (aka N-of-N scheme): All N participants to the multisig account must sign every operation for it to be valid. * Threshold signature (M-of-N scheme): A threshold number of M participants out of the total of N members is required to sign each operation. In either case, there must be at least 2 participants (N >= 2) and at least 2 of them must be required to sign an operation (M >= 2). This tutorial includes two scenarios that show how you can use native multisig accounts. Both scenarios are quite involved and are not intended for daily use by end users. Rather, they are recommended for developers of tools such as staking or baking applications and wallets, desiring to add features related to native multisig accounts. 1. [Registering a baker with a multisig manager key](/tutorials/native-multisig/multisig-baking), using signature aggregation (N-of-N). 2. [Staking by several users](/tutorials/native-multisig/multisig-staking) using threshold signature (M-of-N). These scenarios use the Octez client CLI commands on a testnet. They store the keys unencrypted in the Octez client’s configuration; in production use you should store the keys in an encrypted format. The complete reference for the CLI commands and RPC endpoints associated with this feature is available at [Native multisig accounts](https://octez.tezos.com/docs/alpha/native_multisig.html). Beyond the flagship use cases presented here, multisig accounts may constitute the starting point of fruitful discussions leading to other new exciting applications for the Tezos blockchain. # Part 1: Baking using an aggregated multisig (N-of-N) account This tutorial walks you through setting up a multisig account to use to stake tez and become a baker. It uses a multisig account with signature aggregation (N-of-N scheme), in which all 3 of the 3 participants must approve of operations taken on behalf of the multisig account. These accounts can be controlled by multiple people sharing control of the account or by a single person who wants the additional security of spreading control of the account over multiple keys. For more information about staking, see [Staking](/using/staking). For simplicity, the multisig account uses a separate consensus key for the baker's consensus operations, which allows the baker to sign blocks and attestations without needing the signatures of the participants. In principle, it would be possible to use a multisig account as the consensus key, but doing so is impractical because bakers sign consensus operations frequently and signing transactions for multisig accounts takes time. This is why the Octez tooling does not support using a multisig account as a consensus key. For more information about baking and consensus keys, see [Run a Tezos baker in 5 steps](/tutorials/join-dal-baker). For the purposes of the tutorial, imagine that three users, Alice, Bob, and Charlie want to use a multisig account to bake together. The tutorial follows these general steps that Alice, Bob, and Charlie must do: 1. Creating the multisig account based on their existing `tz4` accounts 2. Revealing the account and registering it as a delegate 3. Staking with the account Each operation for the account (in this case including revealing, registering as a delegate, staking, and any transfers or smart contract calls) must be signed by all 3 participants. For this reason, each operation involves these sub-steps: 1. One participant generates the unsigned operation and shares it with the others. 2. The participants sign the operation and share their signatures. 3. One participant combines the participant signatures into a single signature. 4. That participant signs the operation and submits it to the Tezos network. For the purposes of the tutorial, the "participants" are different accounts on your local machine, stored in the Octez client's local wallet. In a real situation you can use accounts that are hosted on different systems and managed by different wallets. If you have any trouble with the commands on this page, see the [Troubleshooting](#troubleshooting) section. ## Prerequisites Before you begin, make sure that you have version 23 or later of the Octez client and `octez-codec` program installed. See [Installing the Octez client](/developing/octez-client/installing). ## Setting up the multisig account To create a multisig account with signature aggregation, you use two or more existing `tz4` accounts to create a multisig with their aggregated keys. You cannot change the accounts after the multisig account has been created. 1. If you don't have three `tz4` accounts already, create them as usual with the `octez-client gen keys` command, but include the `-s bls` argument to use the BLS signature scheme and create accounts that start with `tz4`. For example, this command creates an account named `alice`: ```bash octez-client gen keys alice -s bls ``` 2. Get the addresses (public key hashes) and public keys of each participant account with the `show address` command: ```bash octez-client show address alice ``` 3. Get the proof of possession (PoP) for each account with the `create bls proof` command, as in this example: ```bash octez-client create bls proof for alice ``` This proof is also generated when the account is revealed, and you can see it in the metadata for the reveal operation. This proof is part of how the accounts provide a public certificate witnessing the shared possession of the tz4 account’s secret key. :::note Two kinds of proofs of possession are used in multisig accounts that use signature aggregation: * PoPs for individual accounts, which are generated with the `create bls proof` command and are used to create the aggregated multisig account in the next step. * PoP fragments of the multisig account, which are generated with the `create bls proof for ... --override-public-key` command. Later, you use these fragments to create the PoP for the multisig account. ::: 4. Aggregate the accounts into the multisig account by passing the public keys and proofs for the participant accounts. This example uses placeholders such as `` and `` for the public key and PoP for each account: ```bash octez-client aggregate bls public keys '[ { "public_key": "", "proof": "" }, { "public_key": "", "proof": "" }, { "public_key": "", "proof": "" } ]' ``` The response includes the public key and address (public key hash) of the multisig account: ```json { "public_key": "", "public_key_hash": "" } ``` 5. Optional: For convenience, add the multisig account to the Octez client with a local alias such as `multisig_aggregate`, using the address from the previous command: ```bash octez-client add address multisig_aggregate ``` 6. Create the aggregated proof of possession (PoP) for the multisig account by combining proof fragments from the participants: 1. Create a proof fragment from each participant account by passing the multisig account's public key to the `create bls proof` command, as in this example: ```bash octez-client create bls proof for alice --override-public-key ``` The response is the fragment of the multisig account's PoP from that participant. 2. Combine the proof fragments into the PoP for the multisig account by passing them to the `aggregate bls proofs` command, as in this example: ```bash octez-client aggregate bls proofs '{ "public_key": "", "proofs": [ "", "", "" ] }' ``` The result is the aggregated proof of possession for the multisig account. The order of the PoPs in this command is not important. 3. Record the PoP because you will need it later. It is represented later by the placeholder ``. :::note The proof of possession in the response is a feature introduced in protocol S that is related to an underlying change in the usage of BLS in the Tezos protocol necessary to implement both these features and aggregated attestations. For more information, start at [tz4: BLS](https://octez.tezos.com/docs/active/accounts.html#tz4-bls) in the Octez and protocol documentation. ::: Now you have a single account controlled by three accounts, none of which can control it independently. In the next section, you use these participant accounts reveal the account and register it as a delegate so it can get baking rights. ## Revealing the account and registering as a delegate Multisig accounts must be revealed and delegated to stake, just like other accounts. However, the process is different, because (as explained at the top of this page) running any operation with the multisig account involves signing the operation with each of the participants. In this section you sign an operation to reveal the account and register as a delegate. 1. Set your installation of the Octez client to use a test network. For example, to use the Shadownet testnet RPC endpoint at https://rpc.shadownet.teztnets.com, run this command: ```bash octez-client -E https://rpc.shadownet.teztnets.com config update ``` For more information about test networks and other RPC endpoints, see [Testing on testnets](/developing/testnets). 2. From the faucet for the testnet, send at least 6,005 tez to the multisig account's address. The account must have some tez to be revealed and to pay for transaction fees on top of the 6,000 tez that you will stake later. 3. Get the counter of the multisig account by running this command, where `` is the address of the multisig account: ```bash octez-client rpc get chains/main/blocks/head/context/contracts//counter ``` 4. Get the hash of a recent block to act as the branch to base the operation on by running this command: ```bash octez-client rpc get chains/main/blocks/head~2/hash ``` The resulting hash is referred to in future steps by the placeholder ``. :::note When you submit the signed operation, this branch must be a sufficiently recent block or the operation fails. The constant `max_operations_time_to_live` determines how recent the branch must be. For example, on Mainnet, this constant is 450 blocks, which gives you about one hour to get the operation signed by enough participants and submitted. To get the value of this constant on the network you are using, run this command, which requires the `jq` program: ```bash octez-client rpc get /chains/main/blocks/head/context/constants | jq | grep max_operations_time_to_live ``` Then, multiply the value of this constant by the value of the `minimal_block_delay` constant to get the time duration in seconds. ::: 5. Generate the operation to reveal the account: ```bash octez-codec encode 024-PtTALLiN.operation.unsigned from '{ "branch": "", "contents": [ { "kind": "reveal", "source": "", "fee": "735", "counter": "", "gas_limit": "3251", "storage_limit": "0", "public_key": "", "proof": "" } ] }' ``` This command uses values from previous steps, including: * The address and public key of the multisig account (`` and ``) * The PoP that was created for the account (``) * The branch (``) * The next counter value, which you can get by adding 1 to the current counter value from a previous step (``) It passes these values to the `octez-codec` program, which encodes them in binary based on the Tallinn protocol. You can replace `024-PtTALLiN.operation.unsigned` with the schema for the protocol that you want to use; see [Encodings](https://octez.tezos.com/docs/developer/encodings.html) in the Octez and protocol documentation. The result is a string of bytes that represents the unsigned operation. These are the bytes that the participant accounts must sign to authorize the operation. 6. Sign the operation as Alice by passing these bytes to the `sign bytes` command, which uses `` as a placeholder for the response from the previous command: ```bash octez-client sign bytes "0x03" for alice ``` Note that this command prefixes the bytes with `0x03` because it is an encoded manager operation. The response is Alice's signature of the transaction. 7. In the same way, sign the same bytes as Bob's account and Charlie's account. 8. Combine the signatures to fully sign the operation by running this command, which uses the placeholders ``, ``, and `` to represent their signatures: ```bash octez-client aggregate bls signatures '{ "public_key": "", "message": "03", "signature_shares": [ "", "", "" ] }' ``` Note that the unsigned operation bytes are prefixed with `03`, not `0x03` as in a previous command. The order of the signatures is not important. The response is the signature for the operation, which starts with `BLsig`. 9. Sign the operation by running this command, which uses the same JSON parameter as the command to generate the operation with the addition of the `signature` field: ```bash octez-codec encode 024-PtTALLiN.operation from '{ "branch": "", "contents": [ { "kind": "reveal", "source": "", "fee": "735", "counter": "", "gas_limit": "3251", "storage_limit": "0", "public_key": "", "proof": "" } ], "signature": "" }' ``` The value of the `signature` field, represented by the placeholder ``, is the output of the previous command. The response is the signed operation as a series of bytes. 10. Submit the operation by passing it to this command: ```bash octez-client rpc post /injection/operation with '""' ``` Note that the signed operation is within both single and double quotes. The response is the hash of the submitted operation. 11. Check the result of the operation by looking it up in the block explorer or passing its hash to the `get receipt` command: ```bash octez-client get receipt for ``` The receipt notes that the operation was signed by the multisig account itself, not by the individual participants. Here is an example receipt: ```shellsession $ octez-client get receipt for oniCMt7XKe9BEsKVz4Yw8YSSNoDrZmRNdR1d2pWLyqNBt7kseCR Warning: This is NOT the Tezos Mainnet. Do NOT use your fundraiser keys on this network. Operation found in block: BL5NWAr6MuF1pYDN6p153wFab8SLWmugf7HgGnFLpkGR5XdN88W (pass: 3, offset: 0) Manager signed operations: From: tz4EoQW3jWNvn2q4kVQawdnCovmnGxJAofeG Fee to the baker: ꜩ0.00145 Expected counter: 679669 Gas limit: 5300 Storage limit: 0 bytes Balance updates: tz4EoQW3jWNvn2q4kVQawdnCovmnGxJAofeG ... -ꜩ0.00145 payload fees(the block proposer) ....... +ꜩ0.00145 Revelation of manager public key: Contract: tz4EoQW3jWNvn2q4kVQawdnCovmnGxJAofeG Key: BLpk1r3rPXZF9w4NohXaPvcR2hEaBEXckVFv2ek6mQdnxr6oxyAdngJED2ueebUHmA6voita4kof Proof of possession: BLsigBUQMG8ZXgH3UA87hTYQ43NctfJk7rA3vEq2uuYQ8Qe7PPgfCqLpJdWxNYzBi1Dxx5xPnwF2uZPFYd4ZmjwmJh8rZGAikymyzYvnhLYE5L1huycuPVgWpLhSEdC1onbJQWCkvbW2YT This revelation was successfully applied Consumed gas: 3249.100 ``` ## Registering as a delegate Now you can set up the multisig account as a delegate with a consensus key. As with the reveal operation, all participants must sign the operation. 1. If you don't have a consensus key selected already, create one with the `octez-client gen keys` command and get its address with the `octez-client show address` command. This key is not required to be a BLS key like the participant accounts. The following steps use the placeholder `` as the public key of the consensus key. 2. Get the counter of the multisig account by running this command, where `` is the address of the multisig account: ```bash octez-client rpc get chains/main/blocks/head/context/contracts//counter ``` 3. Get the hash of a recent block to act as the branch to base the operation on by running this command: ```bash octez-client rpc get chains/main/blocks/head~2/hash ``` 4. As you did in the previous section, generate the operation. You could do the registration and consensus key in separate operations, but doing it in one step illustrates how you can batch operations: ```bash octez-codec encode 024-PtTALLiN.operation.unsigned from '{ "branch": "", "contents": [ { "kind": "delegation", "source": "", "fee": "449", "counter": "", "gas_limit": "1676", "storage_limit": "0", "delegate": "" }, { "kind": "update_consensus_key", "source": "", "fee": "183", "counter": "", "gas_limit": "200", "storage_limit": "0", "pk": "" } ] }' ``` This command uses values from previous steps, including: * The address the multisig account (``) * The branch (``) * The next two counter values, which you can get by adding 1 and 2 to the current counter value from a previous step (`` and ``) * The public key of the consensus key (``) The result is a string of bytes that represents the unsigned operation. :::note This batched operation is equivalent to the usual `octez-client register key` command to set up a non-multisig account as a delegate with a consensus key. Of course, this command doesn't work with multisig accounts because it can't get signatures from all of the participant accounts. ::: 5. As you did in the previous section, sign the bytes as the participant accounts, prefixing the bytes with `0x03`, as in this example: ```bash octez-client sign bytes "0x03" for alice ``` 6. Combine the signatures to fully sign the operation by running this command, which uses the placeholders ``, ``, and `` to represent their signatures: ```bash octez-client aggregate bls signatures '{ "public_key": "", "message": "03", "signature_shares": [ "", "", "" ] }' ``` Note that the unsigned operation bytes are prefixed with `03`, not `0x03` as in a previous command. The response is the signature for the operation, which starts with `BLsig`. 7. Sign the operation by running this command, which uses the same JSON parameter as the command to generate the operation with the addition of the `signature` field: ```bash octez-codec encode 024-PtTALLiN.operation from '{ "branch": "", "contents": [ { "kind": "delegation", "source": "", "fee": "449", "counter": "", "gas_limit": "1676", "storage_limit": "0", "delegate": "" }, { "kind": "update_consensus_key", "source": "", "fee": "183", "counter": "", "gas_limit": "200", "storage_limit": "0", "pk": "" } ], "signature": "" }' ``` The value of the `signature` field, represented by the placeholder ``, is the output of the previous command. The response is the signed operation as a series of bytes. 8. Submit the operation by passing it to this command: ```bash octez-client rpc post /injection/operation with '""' ``` The response is the hash of the submitted operation. 9. Check the result of the operation by looking it up in the block explorer or passing its hash to the `get receipt` command: ```bash octez-client get receipt for ``` Now the account is registered as a delegate and can be a baker. To bake, it needs to stake tez, which you do in the next section. ## Staking with the multisig account Signing the staking operation is similar to signing the other operations with the account. 1. Get the new counter of the multisig account: ```bash octez-client rpc get chains/main/blocks/head/context/contracts//counter ``` 2. Get the hash of a recent block to act as the branch to base the operation on by running this command: ```bash octez-client rpc get chains/main/blocks/head~2/hash ``` 3. Like you did in the previous section, generate the operation to stake with the account: ```bash octez-codec encode 024-PtTALLiN.operation.unsigned from '{ "branch": "", "contents": [ { "kind": "transaction", "source": "", "fee": "808", "counter": "", "gas_limit": "5134", "storage_limit": "0", "amount": "", "destination": "", "parameters": { "entrypoint": "stake", "value": { "prim": "Unit" } } } ] }' ``` This command includes the counter value plus one and the amount to stake in mutez, in this case `6000000000` for 6,000 tez, the minimum amount that bakers must stake. The stake operation is implemented internally as a transfer from the account to itself via the `stake` entrypoint. 4. Sign the operation as each account by passing the operation bytes to the `sign bytes` command: ```bash octez-client sign bytes "0x03" for alice ``` 5. Combine the signatures to fully sign the operation by running this command, which uses the placeholders ``, ``, and `` to represent their signatures: ```bash octez-client aggregate bls signatures '{ "public_key": "", "message": "03", "signature_shares": [ "", "", "" ] }' ``` Note that the unsigned operation bytes are prefixed with `03`, not `0x03` as in a previous command. 6. Sign the operation by running this command, which uses the same JSON parameter as the command to generate the operation with the addition of the `signature` field: ```bash octez-codec encode 024-PtTALLiN.operation from '{ "branch": "", "contents": [ { "kind": "transaction", "source": "", "fee": "808", "counter": "", "gas_limit": "5134", "storage_limit": "0", "amount": "", "destination": "", "parameters": { "entrypoint": "stake", "value": { "prim": "Unit" } } }], "signature": "" }' ``` 7. Submit the operation by passing it to this command: ```bash octez-client rpc post /injection/operation with '""' ``` The response is the hash of the submitted operation. 8. Check the result of the operation by looking it up in the block explorer, checking your staked balance with the `octez-client get staked balance for` command, or passing its hash to the `get receipt` command: ```bash octez-client get receipt for ``` Now the tez is staked and the account can become a baker. When it's time to unstake the tez, the participant accounts must create an unstaking operation and sign it as they signed the other operations. Similarly, they must co-sign any transfers or smart contract calls from this account. To become a baker, see the tutorial [Run a Tezos baker in 5 steps](/tutorials/join-dal-baker). ## Troubleshooting Errors with the `octez-client sign bytes` command: * If this command throws the error `Invalid bytes, expecting hexadecimal notation`, ensure that the bytes to be signed are prefixed with the code `0x03`. Errors with the `octez-client aggregate bls signatures` command: * If this command failed to produce the signature, make sure that the `message` field is prefixed with only `03`, not `0x03`. * Make sure you signed with the correct participant accounts and enough participant accounts. Errors with the command `octez-client rpc post /injection/operation with '""'`: * If this command throws an error that says that the operation failed because it is branched on a block that is too old, the branch that you based the operation on is out of date. You may need to complete these steps more quickly or automate them to complete them in time. You can calculate the allowable time by checking the time to live, as described in [Revealing the account and registering as a delegate](#revealing-the-account-and-registering-as-a-delegate). * If the error says that the operation signature is invalid, make sure that the contents of the operations that you passed to the `octez-codec` commands match. * If the error says that the counter is already used, make sure that you have added one to the current counter value. The `octez-client rpc post /injection/operation` command returns an operation hash but this hash does not exist on the block explorer and the `octez-client get receipt for` command fails: * Check that the source account has enough tez to pay the fees. * The `fee` field for the operation may be too low. ## Summary In this tutorial, starting from existing `tz4` accounts, you created a `tz4` multisig account with its own public key, address, and PoP. No one knows the private key for the account, so no one person can control it. Instead, each subsequent operation must be signed by all participants and aggregated by one participant. The multi-signature scheme ensures that all participants agree on any change in the configuration of the baker. However, the baker executable automatically signs routine consensus operations using another consensus key, that is not subject to the above agreement procedure, which ensures efficiency and seamless execution. From here, you can use the account much as an ordinary user account, but with the enhanced security of requiring multiple signatures to create operations. # Part 2: Staking using a threshold multisig (M-of-N) account This tutorial walks you through setting up a multisig account to use to stake tez. It uses a multisig account with a threshold signature (M-of-N scheme), in which 2 of the 3 participants must approve of operations taken on behalf of the multisig account. These accounts can be controlled by multiple people sharing control of the account or by a single person who wants the additional security of spreading control of the account over multiple keys. For more information about staking, see [Staking](/using/staking). For the purposes of the tutorial, imagine that three users, Alice, Bob, and Charlie want to use a multisig account to control their staking position with a chosen baker. The tutorial follows these general steps that Alice, Bob, and Charlie must do: 1. Creating the multisig account, distributing control over its key to the participant accounts, and destroying the original key 2. Revealing and delegating the account 3. Staking with the account Each operation for the account (in this case including revealing, delegating, staking, and any transfers or smart contract calls) must be signed by 2 participants. For this reason, each operation involves these sub-steps: 1. One participant generates the unsigned operation and shares it with the others. 2. At least two of the participants sign the operation and share their signatures. 3. One participant combines the participant signatures into a single signature. 4. That participant signs the operation and submits it to the Tezos network. For the purposes of the tutorial, the "participants" are different accounts on your local machine, stored in the Octez client's local wallet. In a real situation you can use accounts that are hosted on different systems and managed by different wallets. If you have any trouble with the commands on this page, see the [Troubleshooting](#troubleshooting) section. ## Prerequisites Before you begin, make sure that you have version 23 or later of the Octez client and `octez-codec` program installed. See [Installing the Octez client](/developing/octez-client/installing). ## Setting up the multisig account To create a multisig account, you create an account with the Octez client in a way similar to creating a regular account, but for the multisig account you split control of the account into multiple private keys based on the threshold. The threshold (2 of 3 keys in this case) cannot be changed after it is set. Then you distribute these private keys to the participants, who each import accounts based on them. :::important Whoever has the secret key of the multisig account can sign operations for it independently, getting around the authorization of the participants. For security, after you have imported the participant accounts, ensure that the secret key for the multisig account is destroyed. For this reason, the participants must trust the creator of the multisig account to destroy the private key. ::: 1. Create the unencrypted multisig account by running this command: ```bash octez-client gen keys multisig_staker -s bls ``` This command is like the usual command to create an account except that it specifies the BLS signature scheme and therefore creates an account that starts with `tz4`. :::note The next two steps print unencrypted secret keys to the terminal. Beware of your surroundings and handle the keys with extra care to prevent them from being compromised. ::: 2. Print the address, public key, and private key of the new account: ```bash octez-client show address multisig_staker --show-secret ``` Here is an example result: ``` Hash: tz4SY87c9MTEknnhcmwfu3d7Qu8HqmgwfJkY Public Key: BLpk1tvA7VVVtccUz1VpCcvNgysintAReyaTdEzrCU6Ai9gftLMDQQNniKtmH5sMG7BqeR1uN4hy Secret Key: unencrypted:BLsk31ui8c4tQxfBEyY2FdbQdW8vmpoU5GUT4BhWPYijfjDqTQ3HVJ ``` 3. Split the key to implement the 2-of-3 signature scheme by running this command, where `` is the private key of the multisig account from the previous step: ```bash octez-client share bls secret key between 3 shares with threshold 2 ``` The output includes several fields, including the proof of possession (PoP) of the account and the three private keys for the participant accounts. Here is an example: ```json { "public_key": "", "public_key_hash": "", "proof": "", "secret_shares": [ { "id": 1, "secret_key": "" }, { "id": 2, "secret_key": "" }, { "id": 3, "secret_key": "" } ] } ``` :::note These secret keys are tied to the IDs in the `id` fields in the output. When you submit operations, the signatures must be connected to those IDs, so keep track of the ID of each key. ::: :::note The proof of possession in the response is a feature introduced in protocol proposal S that is related to an underlying change in the usage of BLS in the Tezos protocol necessary to implement both these features and aggregated attestations. For more information, start at [tz4: BLS](https://octez.tezos.com/docs/active/accounts.html#tz4-bls) in the Octez and protocol documentation. ::: 4. Record the PoP of this multisig scheme (represented by the placeholder `` above) because you will need it later. 5. Import the secret keys for the first new account by running the `octez-client import secret key` command and passing the first secret key from the output of the previous step. For example, this command creates an account named `alice` with a private key represented by the placeholder ``: ```bash octez-client import secret key alice unencrypted: ``` Of course, in a real scenario, you would distribute these keys in a secure manner to the other participants, but for the purposes of the tutorial you can use local accounts. 6. Repeat the process to import the secret keys for the other participant accounts. In this tutorial, the examples use `bob` and `charlie` for the local aliases of these accounts. 7. Make sure that you have written down the address of the multisig account (noted above as ``) and its public key (noted above as ``) and then run this command to delete the account and its private key: ```bash octez-client forget address multisig_staker -f ``` :::important As described at the beginning of this section, it's important to delete the private key of the original account because if it still exists, it can be used to sign operations for the multisig account getting around the participant accounts. ::: 8. To keep track of the account's address and public key, add it back as a local alias by running this command, where `` is the public key of the multisig account: ```bash octez-client import public key multisig_staker unencrypted: ``` Now you can get the address of the account with the command `octez-client show address multisig_staker` and use its alias locally, but you don't have its private key. Now you have a single account controlled by three accounts, none of which can control it independently. In the next section, you use these participant accounts to send a transaction on behalf of the multisig account. ## Revealing and delegating the multisig account Multisig accounts must be revealed and delegated in order to stake, just like other accounts. However, the process is different, because (as explained at the top of this page) running any operation with the multisig account involves signing the operation with enough of the participants. In this section you sign an operation to reveal the account and delegate to a baker with whom the participants intend to stake. 1. Set your installation of the Octez client to use a test network. For example, to use the Shadownet testnet RPC endpoint at https://rpc.shadownet.teztnets.com, run this command: ```bash octez-client -E https://rpc.shadownet.teztnets.com config update ``` For more information about test networks and other RPC endpoints, see [Testing on testnets](/developing/testnets). 2. From the faucet for the testnet, send 10,005 tez to the multisig account's address. The account must have some tez to be revealed and to pay for transaction fees on top of the 10,000 tez that you will stake in the next section. 3. Choose a baker to delegate to and stake with. Make sure that the baker accepts external staking. For example, if you are using the Shadownet test network, you can go to https://shadownet.tzkt.io/bakers and find a baker that accepts staking there. The following steps refer to the address of the baker with the placeholder ``. 4. Get the counter of the multisig account by running this command, where `` is the address of the multisig account: ```bash octez-client rpc get chains/main/blocks/head/context/contracts//counter ``` 5. Get the hash of a recent block to act as the branch to base the operation on by running this command: ```bash octez-client rpc get chains/main/blocks/head~2/hash ``` The resulting hash is referred to in future steps by the placeholder ``. :::note When you submit the signed operation, this branch must be a sufficiently recent block or the operation fails. The constant `max_operations_time_to_live` determines how recent the branch must be. For example, on Mainnet, this constant is 450 blocks, which gives you about one hour to get the operation signed by enough participants and submitted. To get the value of this constant on the network you are using, run this command, which requires the `jq` program: ```bash octez-client rpc get /chains/main/blocks/head/context/constants | jq | grep max_operations_time_to_live ``` Then, multiply the value of this constant by the value of the `minimal_block_delay` constant to get the time duration in seconds. ::: 6. Generate the operation to reveal and delegate the account. You could do this in separate operations, but doing it in one step illustrates how you can batch operations: ```bash octez-codec encode 024-PtTALLiN.operation.unsigned from '{ "branch": "", "contents": [ { "kind": "reveal", "source": "", "fee": "735", "counter": "", "gas_limit": "3251", "storage_limit": "0", "public_key": "", "proof": "" }, { "kind": "delegation", "source": "", "fee": "160", "counter": "", "gas_limit": "100", "storage_limit": "0", "delegate": "" } ] }' ``` This command uses values from previous steps, including: * The address and public key of the multisig account (`` and ``) * The PoP that was created when you split the account into participants (``) * The branch (``) * The address of the baker (``) * The next two counter values, which you can get by adding 1 and 2 to the current counter value from a previous step (`` and ``) It passes these values to the `octez-codec` program, which encodes them in binary based on the Tallinn protocol. You can replace `024-PtTALLiN.operation.unsigned` with the schema for the protocol that you want to use; see [Encodings](https://octez.tezos.com/docs/developer/encodings.html) in the Octez and protocol documentation. The result is a string of bytes that represents the unsigned operation. These are the bytes that the participant accounts must sign to authorize the operation. 7. Sign the operation as Alice by passing these bytes to the `sign bytes` command, which uses `` as a placeholder for the response from the previous command: ```bash octez-client sign bytes "0x03" for alice ``` Note that this command prefixes the bytes with `0x03` because it is an encoded manager operation. The response is Alice's signature of the transaction. 8. In the same way, sign the same bytes as Bob's account. 9. Combine Alice and Bob's signatures to fully sign the operation by running this command, which uses the placeholders `` and `` to represent their signatures: ```bash octez-client threshold bls signatures '{ "public_key": "", "message": "03", "signature_shares": [ { "id": 1, "signature": "" }, { "id": 2, "signature": "" } ] }' ``` Note that the `id` fields in this command must match the accounts in the output of the `share bls secret key` command in the previous section. Also note that the unsigned operation bytes are prefixed with `03`, not `0x03` as in a previous command. The response is the signature for the operation, which starts with `BLsig`. 10. Sign the operation by running this command, which uses the same JSON parameter as the command to generate the operation with the addition of the `signature` field: ```bash octez-codec encode 024-PtTALLiN.operation from '{ "branch": "", "contents": [ { "kind": "reveal", "source": "", "fee": "735", "counter": "", "gas_limit": "3251", "storage_limit": "0", "public_key": "", "proof": "" }, { "kind": "delegation", "source": "", "fee": "160", "counter": "", "gas_limit": "100", "storage_limit": "0", "delegate": "" } ], "signature": "" }' ``` The value of the `signature` field, represented by the placeholder ``, is the output of the previous command. The response is the signed operation as a series of bytes. 11. Submit the operation by passing it to this command: ```bash octez-client rpc post /injection/operation with '""' ``` Note that the signed operation is within both single and double quotes. The response is the hash of the submitted operation. 12. Check the result of the operation by looking it up in the block explorer or passing its hash to the `get receipt` command: ```bash octez-client get receipt for ``` The receipt notes that the operation was signed by the multisig account itself, not by the individual participants. It also includes both operations. Here is an example receipt: ```shellsession $ octez-client get receipt for ongej6ymD9Fp6kw98eoRcRx3nc9uYEaAeDuhqQHnXL4KjC7mtXJ Warning: This is NOT the Tezos Mainnet. Do NOT use your fundraiser keys on this network. Operation found in block: BMBj6Pyv2PGXWq9cmYmiSpQxCBohuWA1vUyKN6asw32KHCJ1oNN (pass: 3, offset: 0) Manager signed operations: From: tz4SY87c9MTEknnhcmwfu3d7Qu8HqmgwfJkY Fee to the baker: ꜩ0.000735 Expected counter: 6964 Gas limit: 3251 Storage limit: 0 bytes Balance updates: tz4SY87c9MTEknnhcmwfu3d7Qu8HqmgwfJkY ... -ꜩ0.000735 payload fees(the block proposer) ....... +ꜩ0.000735 Revelation of manager public key: Contract: tz4SY87c9MTEknnhcmwfu3d7Qu8HqmgwfJkY Key: BLpk1tvA7VVVtccUz1VpCcvNgysintAReyaTdEzrCU6Ai9gftLMDQQNniKtmH5sMG7BqeR1uN4hy Proof of possession: BLsigANFeKUgWe2G7xxvu71q1PbBrC3DpugmcA47hb2qNNc3pXgWwG1wqwEKLycEHRy4uxzr3JxvhTmyBspaZy6fAiSFtxf8c31tejMryhJccxXHeJaHvnKhzueraTx4X2BSu61LVUysJW This revelation was successfully applied Consumed gas: 3250.815 Manager signed operations: From: tz4SY87c9MTEknnhcmwfu3d7Qu8HqmgwfJkY Fee to the baker: ꜩ0.00016 Expected counter: 6965 Gas limit: 100 Storage limit: 0 bytes Balance updates: tz4SY87c9MTEknnhcmwfu3d7Qu8HqmgwfJkY ... -ꜩ0.00016 payload fees(the block proposer) ....... +ꜩ0.00016 Delegation: Contract: tz4SY87c9MTEknnhcmwfu3d7Qu8HqmgwfJkY To: tz1TGKSrZrBpND3PELJ43nVdyadoeiM1WMzb This delegation was successfully applied Consumed gas: 100 ``` ## Staking with the multisig account Now that you have the multisig account revealed and delegated, you can stake with it just like a regular user account. Like the previous operations, you must build the operation and get enough participants to sign it. 1. Get the new counter of the multisig account by running this command, where `` is the address of the multisig account: ```bash octez-client rpc get chains/main/blocks/head/context/contracts//counter ``` 2. Get the hash of a recent block to act as the branch to base the operation on by running this command: ```bash octez-client rpc get chains/main/blocks/head~2/hash ``` The resulting hash is referred to in future steps by the placeholder ``. 3. Like you did in the previous section, generate the operation to stake with the account: ```bash octez-codec encode 024-PtTALLiN.operation.unsigned from '{ "branch": "", "contents": [ { "kind": "transaction", "source": "", "fee": "808", "counter": "", "gas_limit": "5134", "storage_limit": "0", "amount": "", "destination": "", "parameters": { "entrypoint": "stake", "value": { "prim": "Unit" } } } ] }' ``` This command includes the counter value plus one and the amount to stake in mutez, in this case `10000000000` for 10,000 tez. The stake operation is implemented internally as a transfer from the account to itself via the `stake` entrypoint. The result is a string of bytes that the participants must sign. 4. Sign the operation as Alice by passing these bytes to the `sign bytes` command, which uses `` as a placeholder for the response from the previous command: ```bash octez-client sign bytes "0x03" for alice ``` Note that this command prefixes the bytes with `0x03` because it is a (batched) manager operation. The response is Alice's signature of the transaction. 5. In the same way, sign the same bytes as Charlie's account. 6. Combine Alice and Charlie's signatures to fully sign the operation by running this command, which uses the placeholders `` and `` to represent their signatures: ```bash octez-client threshold bls signatures '{ "public_key": "", "message": "03", "signature_shares": [ { "id": 1, "signature": "" }, { "id": 3, "signature": "" } ] }' ``` Note that the `id` fields in this command must match the accounts in the output of the `share bls secret key` command in the previous section. Also note that the unsigned operation bytes are prefixed with `03`, not `0x03` as in a previous command. The response is the signature for the operation, which starts with `BLsig`. 7. Sign the operation by running this command, which uses the same JSON parameter as the command to generate the operation with the addition of the `signature` field: ```bash octez-codec encode 024-PtTALLiN.operation from '{ "branch": "", "contents": [ { "kind": "transaction", "source": "", "fee": "808", "counter": "", "gas_limit": "5134", "storage_limit": "0", "amount": "", "destination": "", "parameters": { "entrypoint": "stake", "value": { "prim": "Unit" } } } ], "signature": "" }' ``` The content of this JSON parameter must match the operation you created in a previous step, except for the `signature` field. The response is the signed operation as a series of bytes. 8. Submit the operation by passing it to this command: ```bash octez-client rpc post /injection/operation with '""' ``` Note that the signed operation is within both single and double quotes. The response is the hash of the submitted operation. 9. Check the result of the operation by looking it up in the block explorer or passing its hash to the `get receipt` command: ```bash octez-client get receipt for ``` Now the tez is staked and the account starts receiving rewards automatically. When it's time to unstake the tez, the participant accounts must create an unstaking operation and sign it as they signed the other operations. Similarly, they must co-sign any transfers or smart contract calls from this account. ## Troubleshooting Errors with the `octez-client sign bytes` command: * If this command throws the error `Invalid bytes, expecting hexadecimal notation`, ensure that the bytes to be signed are prefixed with the code `0x03`, as required in the [Micheline encoding](https://octez.tezos.com/docs/shell/micheline.html). Errors with the `octez-client threshold bls signatures` command: * If this command throws an error that says that it failed to produce the signature, make sure that the `message` field is prefixed with only `03` in the JSON encoding, not `0x03`. * Make sure you signed with the correct participant accounts and enough participant accounts. * Make sure that the participant accounts are matched with the correct IDs. Errors with the command `octez-client rpc post /injection/operation with '""'`: * If this command throws an error that says that the operation failed because it is branched on a block that is too old, the branch that you based the operation on is out of date. You may need to complete these steps more quickly or automate them to complete them in time. You can calculate the allowable time by checking the time to live, as described in [Revealing and delegating the multisig account](#revealing-and-delegating-the-multisig-account). * If the error says that the operation signature is invalid, make sure that the contents of the operations that you passed to the `octez-codec` commands match. * If the error says that the counter is already used, make sure that you have added one to the current counter value. The `octez-client rpc post /injection/operation` command returns an operation hash but this hash does not exist on the block explorer and the `octez-client get receipt for` command fails: * Check that the source account has enough tez to pay the fees. * The `fee` field for the operation may be too low. ## Summary In this tutorial you split a single `tz4` account into multiple participant accounts to create a multi-signature account. No one knows the private key for the account, so no one person can control it. Instead, each subsequent operation must be signed by a certain number of participants and aggregated by one participant. From here, you can use the account much as an ordinary user account, but with the enhanced security of requiring multiple signatures to create operations.