diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..544138be4 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "singleQuote": true +} diff --git a/docs/tutorials.mdx b/docs/tutorials.mdx index 6fe44668c..52d5fde80 100644 --- a/docs/tutorials.mdx +++ b/docs/tutorials.mdx @@ -4,8 +4,8 @@ title: Tutorials These tutorials can help you get started developing different kinds of applications on Tezos in as little as 15 minutes. -import TutorialCard from '@site/src/components/TutorialCard'; -import TutorialCardContainer from '@site/src/components/TutorialCardContainer'; +import TutorialCard from "@site/src/components/TutorialCard"; +import TutorialCardContainer from "@site/src/components/TutorialCardContainer"; ## Beginner @@ -13,13 +13,21 @@ These tutorials are intended for developers who are starting work with Tezos: + + +/> +/> @@ -37,13 +45,21 @@ These tutorials contain multiple parts and are intended for developers with some + + +/> +/> +/> @@ -69,13 +85,21 @@ These tutorials are intended for developers who are familiar with Tezos and want + + +/> +/> diff --git a/docs/tutorials/dapp.md b/docs/tutorials/dapp.md new file mode 100644 index 000000000..46b28acf7 --- /dev/null +++ b/docs/tutorials/dapp.md @@ -0,0 +1,56 @@ +--- +title: Create your minimum dapp on Tezos +authors: "Benjamin Fuentes" +last_update: + date: 27 November 2023 +--- + +> dApp : A decentralized application is a type of distributed open source software application that runs on a peer-to-peer (P2P) blockchain network rather than on a single computer. DApps are visibly similar to other software applications that are supported on a website or mobile device. + +This tutorial shows you how to create a poke game on smart contract. +The game consists on poking the owner of a smart contract. The smart contract keeps a track of user interactions and stores a trace. + +Poke sequence diagram. + +```mermaid +sequenceDiagram + Note left of User: Prepare poke transaction + User->>Smartcontract: poke() + Note right of Smartcontract: store(pokeTrace) +``` + +You will learn : + +- How to create a Tezos project with Taqueria. +- How to create a smart contract in jsLigo. +- How to deploy the smart contract a real testnet named Ghostnet. +- How to create a frontend dApp using Taquito library and interact with a Tezos browser wallet. +- How to use an indexer like TZKT. + +## Prerequisites + +This tutorial uses Typescript, so it will be easier if you are familiar with JavaScript. + +1. Make sure that you have installed these tools: + + - [Node.JS and NPM](https://nodejs.org/en/download/): NPM is required to install the web application's dependencies. + - [Taqueria](https://taqueria.io/), version 0.45.0 or later: Taqueria is a platform that makes it easier to develop and test dApps. + - [Docker](https://docs.docker.com/engine/install/): Docker is required to run Taqueria. + - [jq](https://stedolan.github.io/jq/download/): Some commands use the `jq` program to extract JSON data. + - [`yarn`](https://yarnpkg.com/): The frontend application uses yarn to build and run (see this article for details about [differences between `npm` and `yarn`](https://www.geeksforgeeks.org/difference-between-npm-and-yarn/)). + - Any Tezos-compatible wallet that supports Ghostnet, such as [Temple wallet](https://templewallet.com/). + +2. Optionally, you can install [`VS Code`](https://code.visualstudio.com/download) to edit your application code in and the [LIGO VS Code extension](https://marketplace.visualstudio.com/items?itemName=ligolang-publish.ligo-vscode) for LIGO editing features such as code highlighting and completion. + Taqueria also provides a [Taqueria VS Code extension](https://marketplace.visualstudio.com/items?itemName=ecadlabs.taqueria-vscode) that helps visualize your project and run tasks. + +## The tutorial application + +In this tutorial, you create a simple game where the user is poking though a dApp. The user interacts with the smart contract through a web interface, where they can see the current state of the contract and send poke commands to it. The contract responds by updating its storage with the user's address. Alternately, a user can also poke the contract deployed by other users. + +The application looks like this: + +![Example of the table of addresses and which addresses poked them](/img/tutorials/dapp-table.png) + +The code for the completed application is in this GitHub repository: [solution](https://github.com/marigold-dev/training-dapp-1/tree/main/solution) + +When you're ready, move to the next section [Create your minimum dApp on Tezos](./dapp/part-1) to begin setting up the application. diff --git a/docs/tutorials/dapp/part-1.md b/docs/tutorials/dapp/part-1.md new file mode 100644 index 000000000..e32359b1b --- /dev/null +++ b/docs/tutorials/dapp/part-1.md @@ -0,0 +1,638 @@ +--- +title: "Part 1: Create your minimum dApp on Tezos" +authors: "Benjamin Fuentes" +last_update: + date: 27 November 2023 +--- + +To start working with the application, you create a Taqueria project and use it to deploy the Poke contract. +Then you set up a web application to connect with a wallet, and then interact with your smart contract. + +Before you begin, make sure that you have installed the tools in the [Prerequisites](../dapp#prerequisites) section. + +## Creating a Taqueria project + +Taqueria manages the project structure and keeps it up to date. +For example, when you deploy a new smart contract, Taqueria automatically updates the web app to send transactions to that new smart contract. +Follow these steps to set up a Taqueria project: + +On the command-line terminal, run these commands to set up a Taqueria project and install the Ligo and Taquito plugins: + +```bash +taq init training +cd training +taq install @taqueria/plugin-ligo +taq install @taqueria/plugin-taquito +taq create contract pokeGame.jsligo +``` + +## Write the smart contract + +1. Edit the **pokeGame.jsligo** file. Remove the default code and paste this code instead. + + ```ligolang + export type storage = unit; + + type return_ = [list, storage]; + + @entry + const poke = (_: unit, store: storage): return_ => { + return [list([]), store]; + }; + ``` + + Every contract has to follow these rules : + + - At least one entrypoint, annotated with **@entry** , with a mandatory signature taking 2 arguments **\*(parameter, storage)** and a return type. An entrypoint is function that is exposed as an external API. + - **parameter**: the entrypoint parameter. Mandatory and can be of any type. For example: an (ignored) variable starting with`_` here, and of type `unit` (the type void on Ligo). + - **storage**: the on-chain storage. Mandatory and can be of any type. For example, here we use the type `unit`. It is recommended to add an `export` keyword before the type definition as it is a good practice to export it when you require to write unit tests from another Ligo file. + - **return\_**: a mandatory pair of list of `operation` and the storage type (defined earlier). Return type naming is free but don't use an existing keyword like **return**. + + [Have a look on the Entrypoints contracts documentation](/smart-contracts/entrypoints)> + + > Note: Previous versions of LIGO used a single main function instead of a function for each entrypoint. This syntax is still valid, but it is harder to read and deprecated in Ligo V1. + > + > A `Poke` variant parameter is generated from the `poke` entrypoint function under the hood. A variant is more or less equivalent of the Enum type in Javascript. A default main function is generated and act like as a dispatcher for each of your entrypoints. It means that this painful boilerplate is no more needed on the new syntax. + + [Have a look on the Variant type documentation](/smart-contracts/data-types/complex-data-types#variants) + +1. Write the poke function. + The objective is to store every user/caller addresses poking the contract. + Rewrite the storage, and add the caller address to the set of traces. + + At line 1, replace the line with: + + ```ligolang + export type storage = set
; + ``` + +1. Replace the `poke` function with: + + ```ligolang + @entry + const poke = (_: unit, store: storage): return_ => { + return [list([]), Set.add(Tezos.get_source(), store)] + }; + ``` + + Explanation: + + - The Ligo Set library has a function **add** to add one element to the Set of items. There is no concept of Class in Ligo, you use a library to apply functions on objects. + - A list of operation is required to return. An empty list is returned here as there is no other contract to call. + + [Have a look on the Set library documentation](https://ligolang.org/docs/language-basics/sets-lists-tuples#sets) + + [Have a look on the List library documentation](https://ligolang.org/docs/language-basics/sets-lists-tuples/?lang=jsligo#lists) + + Here, get the caller address using `Tezos.get_source()`. Tezos library provides useful functions for manipulating blockchain objects. + + [Have a look on the Tezos library documentation](https://ligolang.org/docs/reference/current-reference) + +## Simulate a call on your smart contract + +The Ligo command-line provides sub-commands to test your Ligo code. + +[Have a look on the Testing Framework documentation](https://ligolang.org/docs/advanced/testing) + +1. Compile the contract with Taqueria (Force to use a specific Ligo version with `TAQ_LIGO_IMAGE` Taqueria environment variable). + +```bash +TAQ_LIGO_IMAGE=ligolang/ligo:1.1.0 taq compile pokeGame.jsligo +``` + +Taqueria is generating the `.tz` Michelson file on the `artifacts` folder. The Michelson language is the default stack language used by the Michelson VM to run your code on a node. It is something similar to WASM. + +[Have a look on the Michelson documentation](https://tezos.gitlab.io/active/michelson.html) + +1. Taqueria is generating two additional files, edit the first file `pokeGame.storageList.jsligo` replacing current code with: + + ```ligolang + #import "pokeGame.jsligo" "Contract" + + const default_storage = Set.empty as set
; + ``` + + When you deploy a contract, you are required to initialize the default state of your smart contract. Taqueria offers you to declare different variables on this file, it is useful to use different initialized state per environment. + + [Have a look on the Taqueria documentation](https://taqueria.io/docs/plugins/plugin-ligo/#the-taq-compile-task) + +1. Compile all (contract + initial storage) + + ```bash + TAQ_LIGO_IMAGE=ligolang/ligo:1.1.0 taq compile pokeGame.jsligo + ``` + + It compiles both source code and storage. + + Before deployment, to simulate a call to our entrypoint **poke**, Taq has a **taq simulate** command. + The contract parameter `Poke()` and the initial storage with the default empty set is passed to the execution. + +1. Edit the second file **pokeGame.parameterList.jsligo** + + ```ligolang + #import "pokeGame.jsligo" "Contract" + const default_parameter: parameter_of Contract = Poke(); + ``` + +1. Run the simulation. First, install the Tezos client plugin, recompile all and then run the simulation. + + ```bash + taq install @taqueria/plugin-octez-client + TAQ_LIGO_IMAGE=ligolang/ligo:1.1.0 taq compile pokeGame.jsligo + taq simulate pokeGame.tz --param pokeGame.parameter.default_parameter.tz + ``` + + Output logs: + + ```logs + ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” + ā”‚ Contract ā”‚ Result ā”‚ + ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ + ā”‚ pokeGame.tz ā”‚ storage ā”‚ + ā”‚ ā”‚ { "tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU" } ā”‚ + ā”‚ ā”‚ emitted operations ā”‚ + ā”‚ ā”‚ ā”‚ + ā”‚ ā”‚ big_map diff ā”‚ + ā”‚ ā”‚ ā”‚ + ā”‚ ā”‚ ā”‚ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + ``` + + You can notice that the instruction is storing the address of the caller into the storage set. + +### Configure your wallet and deploy + +The default Tezos testing testnet is called **Ghostnet**. + +> :warning: You need an account to deploy a contract with some `tez` (the Tezos native currency). The first time you deploy a contract with Taqueria, it is generating a new implicit account with `0 tez`. + +1. Deploy your contract to the `testing` environment. Ut forces Taqueria to generate a default account on a testing config file. + + ```bash + taq deploy pokeGame.tz -e "testing" + ``` + + You should get this kind of log. + + ```log + Warning: the faucet field in network configs has been deprecated and will be ignored. + A keypair with public key hash tz1XXXXXXXXXXXXXXXXXXXXXX was generated for you. + To fund this account: + 1. Go to https://teztnets.xyz and click "Faucet" of the target testnet. + 2. Copy and paste the above key into the 'wallet address field. + 3. Request some Tez (Note that you might need to wait for a few seconds for the network to register the funds). + No operations performed. + ``` + + - Choice NĀ°1 (Recommended): Use alice wallet instead of the generated account. A common usage is to use **alice** account as Taqueria operator. **alice** is a common known address used on Tezos and she has always some **tez**. Replace the Taqueria config file for **testing** env **.taq/config.local.testing.json** with **alice** settings: + + ```json + { + "networkName": "ghostnet", + "accounts": { + "taqOperatorAccount": { + "publicKey": "edpkvGfYw3LyB1UcCahKQk4rF2tvbMUk8GFiTuMjL75uGXrpvKXhjn", + "publicKeyHash": "tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb", + "privateKey": "edsk3QoqBuvdamxouPhin7swCvkQNgq4jP5KZPbwWNnwdZpSpJiEbq" + } + } + } + ``` + + - Choice NĀ°2: use the Taqueria generated account. Copy the account **privateKey** from the **.taq/config.local.testing.json** config file. Open your Temple browser extension on your computer or on your mobile phone and do the [initial setup](https://www.youtube.com/watch?v=S8_tL8PfCts). Once you are done, go to Settings (click on the avatar icon, or display Temple in full page) and click on **Import account > Private key** tab. Paste the **privateKey** to Temple text input and confirm. Send free Tez to your new account via this web faucet [Marigold faucet](https://faucet.marigold.dev/). Connect your wallet on **Ghostnet** and ask for free tez. + + Now you have some money to play with. + +1. Deploy to Ghostnet testnet. + + ```bash + taq deploy pokeGame.tz -e "testing" + ``` + + Your smart contract is deployed on the Ghostnet. + + ```logs + ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” + ā”‚ Contract ā”‚ Address ā”‚ Alias ā”‚ Balance In Mutez ā”‚ Destination ā”‚ + ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ + ā”‚ pokeGame.tz ā”‚ KT1G8tx4qSeJmKRY1p2oxA6eYoCGc9Qi3Fky ā”‚ pokeGame ā”‚ 0 ā”‚ https://ghostnet.ecadinfra.com ā”‚ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + ``` + +## Create the frontend + +### Create a react app + +```bash +yarn create vite +``` + +Then follow the prompts. Choose React and then Typescript+SWC: + +```shell +? Project name: ā€ŗ app #Enter your project name + +? Select a framework: ā€ŗ - Use arrow-keys. Return to submit. # Select React as framework + Vanilla + Vue +āÆ React + Preact + Lit + Svelte + Others + +? Select a variant: ā€ŗ - Use arrow-keys. Return to submit. #Both TypeScript variants are fine. Select TypeScript only. + TypeScript +āÆ TypeScript + SWC + JavaScript + JavaScript + SWC +``` + +[More information about SWC here](https://swc.rs/). + +1. Add taquito and tzkt indexer libraries. + + ```bash + cd app + yarn add @taquito/taquito @taquito/beacon-wallet @airgap/beacon-sdk @tzkt/sdk-api + yarn add -D @airgap/beacon-types + ``` + + > :warning: Before starting, add the following dependencies in order to resolve polyfill issues. Some dependencies are from NodeJs, thus not included in browsers. + +1. For example, in my case, I installed this: + + ```bash + yarn add --dev process buffer crypto-browserify stream-browserify assert stream-http https-browserify os-browserify url path-browserify + ``` + +1. Create a new file `nodeSpecific.ts` in the src folder of your project and edit with this content: + + ```bash + touch src/nodeSpecific.ts + ``` + + ```js + import { Buffer } from "buffer"; + + globalThis.Buffer = Buffer; + ``` + +1. Open the `index.html` file and replace the `body` with this one: + + ```html + +
+ + + + ``` + +1. 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 @airgap/beacon-sdk + // I almost have no idea why it needs `cjs` on dev and `esm` on build, but this is how it works šŸ¤·ā€ā™‚ļø + "@airgap/beacon-sdk": path.resolve( + path.resolve(), + `./node_modules/@airgap/beacon-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. Back to your frontend app, run the dev server. + + ```bash + cd app + yarn dev + ``` + +1. 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 address and his balance. + +Edit **src/App.tsx** file. + +```typescript +import { NetworkType } from "@airgap/beacon-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 = new TezosToolkit("https://ghostnet.tezos.marigold.dev"); + const wallet = new BeaconWallet({ + name: "Training", + preferredNetwork: NetworkType.GHOSTNET, + }); + Tezos.setWalletProvider(wallet); + + 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; +``` + +1. 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. + +1. Edit **ConnectWallet.tsx** + + ```typescript + import { NetworkType } from "@airgap/beacon-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; + }; + + const ConnectButton = ({ + Tezos, + setUserAddress, + setUserBalance, + wallet, + }: ButtonProps): JSX.Element => { + const connectWallet = async (): Promise => { + try { + await wallet.requestPermissions({ + network: { + type: NetworkType.GHOSTNET, + rpcUrl: "https://ghostnet.tezos.marigold.dev", + }, + }); + // gets user's address + const userAddress = await wallet.getPKH(); + const balance = await Tezos.tz.getBalance(userAddress); + setUserBalance(balance.toNumber()); + setUserAddress(userAddress); + } catch (error) { + console.log(error); + } + }; + + return ( +
+ +
+ ); + }; + + export default ConnectButton; + ``` + +1. 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; + ``` + +1. Save both file, 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_. + +1. 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. On 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) + +1. 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. + +1. 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" }, + }) + ); + })(); + }; + ``` + +1. 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}
+ )} +
+ ``` + +1. Save your file and restart your server. + Now, the start script generates the .env file containing the last deployed contract address. + + ```bash + yarn dev + ``` + +1. 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"; + ``` + +1. 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.methods.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. + +1. 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}
)} + ``` + +1. Save and see the page refreshed, then click on the Poke button. + + ![](/img/tutorials/dapp-pokecontracts.png) + + It calls the contract and add your public address tz1... to the set of traces. + +## Summary + +Now, you are able to 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](./part-2). diff --git a/docs/tutorials/dapp/part-2.md b/docs/tutorials/dapp/part-2.md new file mode 100644 index 000000000..3028d368e --- /dev/null +++ b/docs/tutorials/dapp/part-2.md @@ -0,0 +1,643 @@ +--- +title: 'Part 2: Inter-contract calls and testing' +authors: 'Benjamin Fuentes' +last_update: + date: 28 November 2023 +--- + +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 just register the contract's owner 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 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 storage definition by this one: + + ```ligolang + export type pokeMessage = { + receiver : address, + feedback : string + }; + + export type storage = { + pokeTraces : map, + feedback : string + }; + ``` + +1. Replace your poke function with theses lines: + + ```ligolang + @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. [Have a look on the Functional updates documentation](https://ligolang.org/docs/language-basics/maps-records/#functional-updates). Note: you cannot do assignment like this `store.pokeTraces=...` in jsLigo, there are no concept of Classes, use `Functional updates` instead. + - `Map.add(...`: Add a key, value entry to a map. For more information about [Map](https://ligolang.org/docs/language-basics/maps-records/#maps). + - `export type storage = {...};` a `Record` type is declared, it is an [object structure](https://ligolang.org/docs/language-basics/maps-records#records). + - `Tezos.get_self_address()` is a native function that returns the current contract address running this code. Have a look on [Tezos native functions](https://ligolang.org/docs/reference/current-reference). + - `feedback: ""`: poking directly does not store feedbacks. + +1. Edit `pokeGame.storageList.jsligo` to change the storage initialization. + + ```ligolang + #import "pokeGame.jsligo" "Contract" + + const default_storage: Contract.storage = { + pokeTraces: Map.empty as map, + feedback: "kiss" + }; + ``` + +1. Compile your contract. + + ```bash + TAQ_LIGO_IMAGE=ligolang/ligo:1.1.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://ligolang.org/docs/advanced/testing) + > :information_source: Test module with specific functions [here](https://ligolang.org/docs/reference/test) + +1. Edit the file. + + ```ligolang + #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 module in order 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 return 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. + +1. Run the test. + + ```bash + TAQ_LIGO_IMAGE=ligolang/ligo:1.1.0 taq test unit_pokeGame.jsligo + ``` + + 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 call 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 in 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: + + ```ligolang + 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. + +1. Write the missing functions, starting with `getFeedback`. Add this new function at the end of the file. + + ```ligolang + @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). + +1. Add now, the first part of the function `pokeAndGetFeedback`. + + ```ligolang + @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. + +1. Write the last missing function `pokeAndGetFeedbackCallback`, receive the feedback and finally store it. + + ```ligolang + @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. + +1. Compile the contract. + + > Note: Remove the file `pokeGame.parameterList.jsligo` to remove all unnecessary error logs as there is need to maintain this file anymore. + + ```bash + TAQ_LIGO_IMAGE=ligolang/ligo:1.1.0 taq compile pokeGame.jsligo + ``` + +1. (Optional) Write a unit test for this new function `pokeAndGetFeedback`. + +## Use views instead of inter-contract call + +As you saw on the previous step, inter-contract calls makes the business logic more complex but not only, [thinking about the cost is even worst](https://ligolang.org/docs/tutorials/inter-contract-calls/?lang=jsligo#a-note-on-complexity). + +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 documentation here about onchain views](https://ligolang.org/docs/protocol/hangzhou#on-chain-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. + + ```ligolang + @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") + }; + }; + ``` + +1. Declare the view at the end of the file. Do not forget the annotation `@view` ! + + ```ligolang + @view + export const feedback = (_: unit, store: storage): string => { return store.feedback }; + ``` + +1. Compile the contract. + + ```bash + TAQ_LIGO_IMAGE=ligolang/ligo:1.1.0 taq compile pokeGame.jsligo + ``` + +1. (Optional) Write a unit test for the updated function `pokeAndGetFeedback`. + +## Write mutation tests + +Ligo provides mutations testing through the Test library. Mutation tests are like `testing your tests` to see if your unit tests 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 + ``` + +1. Edit the file. + + ```ligolang + #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 [module doc](https://ligolang.org/docs/language-basics/modules). + - `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 empty list (good, even mutants did not harm your code). + +1. Run the test. + + ```bash + TAQ_LIGO_IMAGE=ligolang/ligo:1.1.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 which 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. + +1. Go to your source file pokeGame.jsligo, and annotate the function `pokeAndGetFeedback` with `@no_mutation`. + + ```ligolang + @no_mutation + @entry + const pokeAndGetFeedback ... + ``` + +1. Run again the mutation tests. + + ```bash + TAQ_LIGO_IMAGE=ligolang/ligo:1.1.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 [previous session](https://github.com/marigold-dev/training-dapp-1/tree/main/solution/app). + +1. 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.1.0 taq compile pokeGame.jsligo + taq generate types ./app/src + taq deploy pokeGame.tz -e "testing" + ``` + +1. Adapt the frontend application code. Edit `App.tsx`, and add new import. + + ```typescript + import { address } from './type-aliases'; + ``` + +1. Add new React variable after `userBalance` definition. + + ```typescript + const [contractToPoke, setContractToPoke] = useState(''); + ``` + +1. 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.methods + .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)}`); + } + }; + ``` + +1. 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' />
+ ``` + +1. 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 a 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 a feedback from a targeted contract (as input of the form) via any listed contract (the first column of the table). + +1. Refresh manually clicking on `Fetch contracts` button. + + Poke other developer's contract to discover their contract hidden feedback when you poke them. + +## Summary + +Now, you are able to call other contracts, use views and test you smart contract before deploying it. + +On next training, you will learn how to use tickets. + +When you are ready, continue to [Part 3: Tickets](./part-3). diff --git a/docs/tutorials/dapp/part-3.md b/docs/tutorials/dapp/part-3.md new file mode 100644 index 000000000..be7bd5c8a --- /dev/null +++ b/docs/tutorials/dapp/part-3.md @@ -0,0 +1,665 @@ +--- +title: "Part 3: Tickets" +authors: "Benjamin Fuentes" +last_update: + date: 29 November 2023 +--- + +Previously, you learned how to do inter-contract calls, use view 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. Ticket 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 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 from same type are all equals and can be merged into a single ticket. When ticket types are different then it is no more comparable. +- Transferable: you can send ticket into a Transaction parameter. +- Storable: only on smart contract storage for the moment (Note: a new protocol release will enable it for implicit account soon). +- Non dupable: you cannot copy or duplicate a ticket, it is a unique singleton object living in specific blockchain instance. +- Splittable: if amount is > 2 then you can split ticket object into 2 objects. +- Mergeable: you can merge ticket from same ticketer and 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 Authentication. Adding some claims/rules on the ticket provides you some rights. +- Simplified FA1.2/FA2 token: representing crypto token with tickets (mint/burn/split/join), but it does not have all same properties and does not respect the TZIP standard. +- Voting rights: giving 1 ticket that count 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 ticket from void. In general, minting operations are done by administrators of smart contract 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. + + ```ligolang + export type storage = { + pokeTraces: map, + feedback: string, + ticketOwnership: map> //ticket of claims + }; + ``` + + In order to fill this map, add an 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 are generating `DUP` command. To avoid our contract to fail at runtime, Ligo parses statically our code during compilation time to detect any DUP on tickets. + + To solve most of issues, segregate ticket objects from the rest of the storage, or structures containing ticket objects in order to avoid compilation errors. To do this, just destructure any object until you get tickets isolated. + + For each function having the storage as parameter, `store` object need to be destructured to isolate `ticketOwnership` object holding our tickets. Then, don't use anymore the `store` object or it creates a **DUP** error. + +1. Add the new `Init` function. + + ```ligolang + @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) } + ] + } + }; + ``` + + Init function looks at how many tickets to create from the current caller, then it is added to the current map. + +1. Modify the poke function. + + ```ligolang + @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 Map library to extract the item from the map and avoid any copy. + + > Note: more information about this function [here](https://ligolang.org/docs/reference/map-reference). + + On a second step, look at the optional ticket, if it exists, then burn it (i.e do not store it somewhere on the storage anymore) and add a trace of execution, otherwise fail with an error message. + +1. Same for `pokeAndGetFeedback` function, do same checks and type modifications as below. + + ```ligolang + @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") + } + } + }; + ``` + +1. Update the storage initialization on `pokeGame.storageList.jsligo`. + + ```ligolang + #import "pokeGame.jsligo" "Contract" + + const default_storage = { + pokeTraces: Map.empty as map, + feedback: "kiss", + ticketOwnership: Map.empty as + map< + address, + ticket + > //ticket of claims + + }; + ``` + +1. Compile the contract to check any errors. + + > Note: don't forget to check that Docker is running for taqueria. + + ```bash + npm i + + TAQ_LIGO_IMAGE=ligolang/ligo:1.1.0 taq compile pokeGame.jsligo + ``` + + Check on logs that everything is fine . + + Try to display a DUP error now. + +1. Add this line on `poke function` just after the first line of storage destructuration `const { pokeTraces, feedback, ticketOwnership } = store;`. + + ```ligolang + const t2 = Map.find_opt(Tezos.get_source(), ticketOwnership); + ``` + +1. Compile again. + + ```bash + TAQ_LIGO_IMAGE=ligolang/ligo:1.1.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. + ``` + +1. Remove it. + +## Test your code + +Update the unit tests files to see if you can still poke. + +1. Edit the `./contracts/unit_pokeGame.jsligo` file. + + ```ligolang + #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. + +1. Run the test, and look at the logs to track execution. + + ```bash + TAQ_LIGO_IMAGE=ligolang/ligo:1.1.0 taq test unit_pokeGame.jsligo + ``` + + 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 šŸŽ‰ ā”‚ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + ``` + +1. Redeploy the smart contract. + + Let play with the CLI to compile and deploy. + + ```bash + TAQ_LIGO_IMAGE=ligolang/ligo:1.1.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 cannot use the app anymore without tickets. + + ```bash + cd app + yarn dev + ``` + +1. Connect with any wallet with enough Tez, and Poke your own 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. + +1. Add a new button for minting on a specific contract, replace the full content of `App.tsx` with: + + ```typescript + import { NetworkType } from "@airgap/beacon-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 = new TezosToolkit("https://ghostnet.tezos.marigold.dev"); + const wallet = new BeaconWallet({ + name: "Training", + preferredNetwork: NetworkType.GHOSTNET, + }); + Tezos.setWalletProvider(wallet); + + 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.methods + .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) => ( + + + + + + ))} + +
addresstrace "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. + +1. Refresh the page, now that you have the Mint button. + +1. Mint a ticket on this contract. + + ![mint](/img/tutorials/dapp-mint.png) + +1. Wait for the Tx popup confirmation and then try to poke again, it should succeed now. + + ![success](/img/tutorials/dapp-success.png) + +1. 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) + + Congratulation, 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 are able to understand ticket. If you want to learn more about tickets, read this great article [here](https://www.marigold.dev/post/tickets-for-dummies). + +On next training, you will learn how to upgrade smart contracts. + +When you are ready, continue to [Part 4: Smart contract upgrades](./part-4). diff --git a/docs/tutorials/dapp/part-4.md b/docs/tutorials/dapp/part-4.md new file mode 100644 index 000000000..a4a672703 --- /dev/null +++ b/docs/tutorials/dapp/part-4.md @@ -0,0 +1,1573 @@ +--- +title: "Part 4: Smart contract upgrades" +authors: "Benjamin Fuentes" +last_update: + date: 29 November 2023 +--- + +# 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 maybe 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 to upgrade a contract. + +# Prerequisites + +There is nothing more than you needed on 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 application lifecycle implies to evolve and upgrade code to fix bug 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 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. + + ```ligolang + export type feedbackFunction = (oracleAddress: address) => string; + + export type storage = { + pokeTraces: map, + feedback: string, + ticketOwnership: map>, //ticket of claims + feedbackFunction: feedbackFunction + }; + ``` + + Let's do minor changes as you have 1 additional field `feedbackFunction` on storage destructuring. + +1. Edit the `PokeAndGetFeedback` function where the lambda `feedbackFunction(..)` is executed + + ```ligolang + @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. + +1. To modify the lambda function code, add an extra admin entrypoint `updateFeedbackFunction`. + + ```ligolang + @entry + const updateFeedbackFunction = (newCode: feedbackFunction, store: storage): return_ => { + const { pokeTraces, feedback, ticketOwnership, feedbackFunction } = store; + ignore(feedbackFunction); + return [ + list([]), + { pokeTraces, feedback, ticketOwnership, feedbackFunction: newCode } + ] + }; + ``` + +1. The storage definition is broken, fix all storage missing field warnings on `poke` and `init` functions. + + ```ligolang + @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 + } + ] + } + }; + ``` + +1. Change the initial storage with the old initial value of the lambda function (i.e calling a view to get a feedback). + + ```ligolang + #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" + ) + }; + } + ) + }; + ``` + +1. Compile and play with the CLI. + + ```bash + npm i + TAQ_LIGO_IMAGE=ligolang/ligo:1.1.0 taq compile pokeGame.jsligo + ``` + +1. 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 ā”‚ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + ``` + +1. Test the dApp frontend. + + Regenerate types and run the frontend. + + ```bash + taq generate types ./app/src + cd app + yarn dev + ``` + +1. Run the user sequence on the web page: + + 1. Mint 1 ticket. + 1. wait for confirmation. + 1. poke a contract address. + 1. wait for confirmation. + 1. click on button to refresh the contract list. + So far so good, you have the same result as previous training . + + Update the lambda function in background with the CLI though the new admin entrypoint. Return a fixed string this time, just for demo purpose and verify that the lambda executed is returning another output. + +1. Edit the file `pokeGame.parameterList.jsligo`. + + ```ligolang + #import "pokeGame.jsligo" "Contract" + const default_parameter : parameter_of Contract = UpdateFeedbackFunction((_oracleAddress : address) : string => "YEAH!!!"); + ``` + +1. Compile all and call an init transaction. + + ```bash + TAQ_LIGO_IMAGE=ligolang/ligo:1.1.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 ā”‚ + ā”‚ ā”‚ ā”‚ ā”‚ ā”‚ ā”‚ ā”‚ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + ``` + +1. Run the user sequence on the web page: + + 1. Mint 1 ticket. + 1. Wait for confirmation. + 1. Poke a contract address. + 1. Wait for confirmation. + 1. Click on button to refresh the contract list. + + You see that the feedback has changed to `YEAH!!!`. + +1. Optional: fix the units tests. + +## Proxy pattern + +The goal is to have a proxy contract maintaining the application lifecycle, it is an enhancement of previous naive solution. +Deploy a complete new smart contract, but this time, the end user is not interacting directly with this contract. Instead, the proxy becomes the default entrypoint and keep 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. + +1. Remove pokeGame.parameterList.jsligo. + +1. Get back the original version of `pokeGame.jsligo` from previous training as it is easier to start from here. + +1. Create a new file `tzip18.jsligo`. + + ```bash + taq create contract tzip18.jsligo + ``` + +1. Edit the file. + + ```ligolang + // 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. + +1. Get back to `pokeGame.jsligo` and import this file on first line. + + ```ligolang + #import "./tzip18.jsligo" "TZIP18" + ``` + +1. Add the type on the storage definition. + + ```ligolang + export type storage = { + pokeTraces: map, + feedback: string, + ticketOwnership: map>, //ticket of claims + tzip18: TZIP18.tzip18 + }; + ``` + +1. Fix all missing field tzip18 on storage structure in the file. + + ```ligolang + 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 an 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**. + +1. Write a main function annotated with @entry. + The parameter is a string representing the entrypoint name and some generic bytes that 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. + + ```ligolang + @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 storage (looking at `tzip18.contractPrevious` field ). + - With no more variant, the pattern matching is broken and `if...else` statement has 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 payload structure for each endpoint. + +1. Add the last missing function changing the version of this contract and make it obsolete (just before the main function). + +```ligolang +/** + * 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 a `if...else` on `viewName` argument. + +```ligolang +@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. + +```ligolang +#import "pokeGame.jsligo" "Contract" + +const default_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.1.0 taq compile pokeGame.jsligo +``` + +All good. + +#### Write the unique proxy + +1. Create a file `proxy.jsligo`. + + ```bash + taq create contract proxy.jsligo + ``` + +1. Define the storage and entrypoints on it. + + ```ligolang + export type storage = { + governance: address, //admins + entrypoints: big_map //interface schema map + }; + + type _return = [list, storage]; + ``` + + The storage: + + - Holds a /or several admin. + - 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. + +1. Add our missing types just above. + + ```ligolang + 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). + +1. Add the `Call`entrypoint (simple forward). (Before main function). + + ```ligolang + // 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 forward it to the right location. + +1. Then, write the `upgrade` entrypoint. (Before main function). + + ```ligolang + /** + * 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). + +1. Last change is to expose any view from underlying contract, declare it at the end of the file. + +```ligolang +@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.1.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 !!!**). + +```ligolang +#include "proxy.jsligo" +const default_storage = { + governance : "tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb" as address, //admins + entrypoints : Big_map.empty as big_map //interface schema map +}; +``` + +1. Compile and deploy it. + + ```bash + TAQ_LIGO_IMAGE=ligolang/ligo:1.1.0 taq compile proxy.jsligo + taq deploy proxy.tz -e testing + ``` + + ```logs + ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” + ā”‚ Contract ā”‚ Address ā”‚ Alias ā”‚ Balance In Mutez ā”‚ Destination ā”‚ + ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ + ā”‚ proxy.tz ā”‚ KT1BPoz3Yi8LPimxCiDvpmutbCNY8x3ghKyQ ā”‚ proxy ā”‚ 0 ā”‚ https://ghostnet.ecadinfra.com ā”‚ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + ``` + + Keep this **proxy address**, as you need to report it below on `tzip18.proxy` field. + +1. Deploy a smart contract V1. ( :warning: Change with the **proxy address** on the file `pokeGame.storageList.jsligo` like here below ). + + ```ligolang + #import "pokeGame.jsligo" "Contract" + + const default_storage = { + pokeTraces: Map.empty as map, + feedback: "kiss", + ticketOwnership: Map.empty as map>, //ticket of claims + tzip18: { + proxy: "KT1BPoz3Yi8LPimxCiDvpmutbCNY8x3ghKyQ" as address, + version: 1 as nat, + contractPrevious: None() as option
, + contractNext: None() as option
+ } + }; + ``` + +1. Deploy the underlying V1 contract. + + ```bash + TAQ_LIGO_IMAGE=ligolang/ligo:1.1.0 taq compile pokeGame.jsligo + taq deploy pokeGame.tz -e testing + ``` + + ```logs + ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” + ā”‚ Contract ā”‚ Address ā”‚ Alias ā”‚ Balance In Mutez ā”‚ Destination ā”‚ + ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ + ā”‚ pokeGame.tz ā”‚ KT18ceGtUsNtQTk9smxQcaxAswRVkHDDKDgK ā”‚ pokeGame ā”‚ 0 ā”‚ https://ghostnet.ecadinfra.com ā”‚ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + ``` + +1. 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 :warning:). + + ```ligolang + #import "proxy.jsligo" "Contract" + + const initProxyWithV1: parameter_of Contract = + Upgrade( + [ + list( + [ + { + name: "Poke", + isRemoved: false, + entrypoint: Some( + { + method: "Poke", + addr: "KT18ceGtUsNtQTk9smxQcaxAswRVkHDDKDgK" as + address + } + ) + }, + { + name: "PokeAndGetFeedback", + isRemoved: false, + entrypoint: Some( + { + method: "PokeAndGetFeedback", + addr: "KT18ceGtUsNtQTk9smxQcaxAswRVkHDDKDgK" as + address + } + ) + }, + { + name: "Init", + isRemoved: false, + entrypoint: Some( + { + method: "Init", + addr: "KT18ceGtUsNtQTk9smxQcaxAswRVkHDDKDgK" as + address + } + ) + }, + { + name: "changeVersion", + isRemoved: false, + entrypoint: Some( + { + method: "changeVersion", + addr: "KT18ceGtUsNtQTk9smxQcaxAswRVkHDDKDgK" as + address + } + ) + }, + { + name: "feedback", + isRemoved: false, + entrypoint: Some( + { + method: "feedback", + addr: "KT18ceGtUsNtQTk9smxQcaxAswRVkHDDKDgK" as + address + } + ) + } + ] + ) as list, + None() as option + ] + ); + ``` + +1. Compile & Call it. + + ```bash + TAQ_LIGO_IMAGE=ligolang/ligo:1.1.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 ā”‚ KT1BPoz3Yi8LPimxCiDvpmutbCNY8x3ghKyQ ā”‚ (Left (Pair { Pair "Poke" False (Some (Pair "Poke" "KT18ceGtUsNtQTk9smxQcaxAswRVkHDDKDgK")) ; ā”‚ default ā”‚ 0 ā”‚ https://ghostnet.ecadinfra.com ā”‚ + ā”‚ ā”‚ ā”‚ Pair "PokeAndGetFeedback" ā”‚ ā”‚ ā”‚ ā”‚ + ā”‚ ā”‚ ā”‚ False ā”‚ ā”‚ ā”‚ ā”‚ + ā”‚ ā”‚ ā”‚ (Some (Pair "PokeAndGetFeedback" "KT18ceGtUsNtQTk9smxQcaxAswRVkHDDKDgK")) ; ā”‚ ā”‚ ā”‚ ā”‚ + ā”‚ ā”‚ ā”‚ Pair "Init" False (Some (Pair "Init" "KT18ceGtUsNtQTk9smxQcaxAswRVkHDDKDgK")) ; ā”‚ ā”‚ ā”‚ ā”‚ + ā”‚ ā”‚ ā”‚ Pair "changeVersion" ā”‚ ā”‚ ā”‚ ā”‚ + ā”‚ ā”‚ ā”‚ False ā”‚ ā”‚ ā”‚ ā”‚ + ā”‚ ā”‚ ā”‚ (Some (Pair "changeVersion" "KT18ceGtUsNtQTk9smxQcaxAswRVkHDDKDgK")) ; ā”‚ ā”‚ ā”‚ ā”‚ + ā”‚ ā”‚ ā”‚ Pair "feedback" False (Some (Pair "feedback" "KT18ceGtUsNtQTk9smxQcaxAswRVkHDDKDgK")) } ā”‚ ā”‚ ā”‚ ā”‚ + ā”‚ ā”‚ ā”‚ None)) ā”‚ ā”‚ ā”‚ ā”‚ + ā”‚ ā”‚ ā”‚ ā”‚ ā”‚ ā”‚ ā”‚ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + ``` + +#### Update the frontend + +1. Go on frontend side, recompile all and generate typescript classes. + + ```bash + TAQ_LIGO_IMAGE=ligolang/ligo:1.1.0 taq compile pokeGame.jsligo + TAQ_LIGO_IMAGE=ligolang/ligo:1.1.0 taq compile proxy.jsligo + taq generate types ./app/src + ``` + +1. Change the script to extract the proxy address instead of the contract one, edit `./app/package.json` and replace the line of script by: + + ```json + "dev": "jq -r -f filter.jq ../.taq/testing-state.json > .env && vite", + ``` + +1. Where you created a new file `filter.jq` with below content. + + ```bash + echo '"VITE_CONTRACT_ADDRESS=" + last(.tasks[] | select(.task == "deploy" and .output[0].contract == "proxy.tz").output[0].address)' > ./app/filter.jq + ``` + +1. Edit `./app/src/App.tsx` and change the contract address, display, etc ... + + ```typescript + import { NetworkType } from "@airgap/beacon-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 = new TezosToolkit("https://ghostnet.tezos.marigold.dev"); + const wallet = new BeaconWallet({ + name: "Training", + preferredNetwork: NetworkType.GHOSTNET, + }); + Tezos.setWalletProvider(wallet); + + 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) => ( + + + + + + ))} + +
addresstrace "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; + ``` + + - 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 exposed entrypoint is altered. As all is generic, now on the proxy side there are only `await c.methods.callContract("my_entrypoint_name",my_packed_payload_bytes).send()` calls. + +1. Run the frontend locally. + +```bash +cd app +yarn dev +``` + +6. Do all the same actions as before through the proxy. + +1. Login. +1. Refresh the contract list. +1. Mint 1 ticket. +1. Wait for confirmation popup. +1. Poke. +1. Wait for confirmation popup. +1. 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 on it. Don't forget again to change `proxy` and `contractPrevious` by our own values ! + +```ligolang +const storageV2 = { + pokeTraces: Map.empty as map, + feedback: "hello", + ticketOwnership: Map.empty as map>, + tzip18: { + proxy: "KT1BPoz3Yi8LPimxCiDvpmutbCNY8x3ghKyQ" as address, + version: 2 as nat, + contractPrevious: Some( + "KT18ceGtUsNtQTk9smxQcaxAswRVkHDDKDgK" as address + ) as option
, + contractNext: None() as option
, + }, +}; +``` + +```bash +TAQ_LIGO_IMAGE=ligolang/ligo:1.1.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 ā”‚ KT1QXXwzRYwrvtDAJpT1jnxym86YbhzMHnKF ā”‚ 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. + +```ligolang +const initProxyWithV2: parameter_of Contract = + Upgrade( + [ + list( + [ + { + name: "Poke", + isRemoved: false, + entrypoint: Some( + { + method: "Poke", + addr: "KT1QXXwzRYwrvtDAJpT1jnxym86YbhzMHnKF" as + address + } + ) + }, + { + name: "PokeAndGetFeedback", + isRemoved: false, + entrypoint: Some( + { + method: "PokeAndGetFeedback", + addr: "KT1QXXwzRYwrvtDAJpT1jnxym86YbhzMHnKF" as + address + } + ) + }, + { + name: "Init", + isRemoved: false, + entrypoint: Some( + { + method: "Init", + addr: "KT1QXXwzRYwrvtDAJpT1jnxym86YbhzMHnKF" as + address + } + ) + }, + { + name: "changeVersion", + isRemoved: false, + entrypoint: Some( + { + method: "changeVersion", + addr: "KT1QXXwzRYwrvtDAJpT1jnxym86YbhzMHnKF" as + address + } + ) + }, + { + name: "feedback", + isRemoved: false, + entrypoint: Some( + { + method: "feedback", + addr: "KT1QXXwzRYwrvtDAJpT1jnxym86YbhzMHnKF" as + address + } + ) + } + ] + ) as list, + None() as option + ] + ); +``` + +9. Call the proxy to do the changes. + +```bash +TAQ_LIGO_IMAGE=ligolang/ligo:1.1.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 ā”‚ KT1BPoz3Yi8LPimxCiDvpmutbCNY8x3ghKyQ ā”‚ (Left (Pair { Pair "Poke" False (Some (Pair "Poke" "KT1QXXwzRYwrvtDAJpT1jnxym86YbhzMHnKF")) ; ā”‚ default ā”‚ 0 ā”‚ https://ghostnet.ecadinfra.com ā”‚ +ā”‚ ā”‚ ā”‚ Pair "PokeAndGetFeedback" ā”‚ ā”‚ ā”‚ ā”‚ +ā”‚ ā”‚ ā”‚ False ā”‚ ā”‚ ā”‚ ā”‚ +ā”‚ ā”‚ ā”‚ (Some (Pair "PokeAndGetFeedback" "KT1QXXwzRYwrvtDAJpT1jnxym86YbhzMHnKF")) ; ā”‚ ā”‚ ā”‚ ā”‚ +ā”‚ ā”‚ ā”‚ Pair "Init" False (Some (Pair "Init" "KT1QXXwzRYwrvtDAJpT1jnxym86YbhzMHnKF")) ; ā”‚ ā”‚ ā”‚ ā”‚ +ā”‚ ā”‚ ā”‚ Pair "changeVersion" ā”‚ ā”‚ ā”‚ ā”‚ +ā”‚ ā”‚ ā”‚ False ā”‚ ā”‚ ā”‚ ā”‚ +ā”‚ ā”‚ ā”‚ (Some (Pair "changeVersion" "KT1QXXwzRYwrvtDAJpT1jnxym86YbhzMHnKF")) ; ā”‚ ā”‚ ā”‚ ā”‚ +ā”‚ ā”‚ ā”‚ Pair "feedback" False (Some (Pair "feedback" "KT1QXXwzRYwrvtDAJpT1jnxym86YbhzMHnKF")) } ā”‚ ā”‚ ā”‚ ā”‚ +ā”‚ ā”‚ ā”‚ None)) ā”‚ ā”‚ ā”‚ ā”‚ +ā”‚ ā”‚ ā”‚ ā”‚ ā”‚ ā”‚ ā”‚ +ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ +``` + +11. Back to the web app, test the flow again: + +1. Refresh the contract list. +1. Mint 1 ticket. +1. Wait for confirmation popup. +1. Poke. +1. Wait for confirmation popup. +1. Refresh the contract list. + +Now, the proxy is calling the contract V2 and should return `hello` on the traces and no more `kiss`. + +#### Last part is to set the old smart contract as obsolete + +1. Add a new parameter on `proxy.parameterList.jsligo` to force change of version on old contract (:warning: replace below with your own addresses for V1 ad V2). + +```ligolang +const changeVersionV1ToV2: parameter_of Contract = + Upgrade( + [ + list([]) as list, + Some( + { + oldAddr: "KT18ceGtUsNtQTk9smxQcaxAswRVkHDDKDgK" as address, + newAddr: "KT1QXXwzRYwrvtDAJpT1jnxym86YbhzMHnKF" as address + } + ) as option + ] + ); +``` + +1. Compile. + + ```bash + TAQ_LIGO_IMAGE=ligolang/ligo:1.1.0 taq compile proxy.jsligo + taq call proxy --param proxy.parameter.changeVersionV1ToV2.tz -e testing + ``` + +1. Check logs. + + ```logs + ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” + ā”‚ Contract Alias ā”‚ Contract Address ā”‚ Parameter ā”‚ Entrypoint ā”‚ Mutez Transfer ā”‚ Destination ā”‚ + ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ + ā”‚ proxy ā”‚ KT1BPoz3Yi8LPimxCiDvpmutbCNY8x3ghKyQ ā”‚ (Left (Pair {} ā”‚ default ā”‚ 0 ā”‚ https://ghostnet.ecadinfra.com ā”‚ + ā”‚ ā”‚ ā”‚ (Some (Pair "KT18ceGtUsNtQTk9smxQcaxAswRVkHDDKDgK" "KT1QXXwzRYwrvtDAJpT1jnxym86YbhzMHnKF")))) ā”‚ ā”‚ ā”‚ ā”‚ + ā”‚ ā”‚ ā”‚ ā”‚ ā”‚ ā”‚ ā”‚ + ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ + ``` + +1. 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 more "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 OPS side. + +That's your tradeoff. + +## Summary + +Now, you are able to upgrade deployed contracts. diff --git a/sidebars.js b/sidebars.js index 5c7820330..50001f939 100644 --- a/sidebars.js +++ b/sidebars.js @@ -32,10 +32,7 @@ const sidebars = { id: 'architecture/tokens', type: 'doc', }, - items: [ - 'architecture/tokens/FA1.2', - 'architecture/tokens/FA2', - ], + items: ['architecture/tokens/FA1.2', 'architecture/tokens/FA2'], }, // { // TODO // type: 'category', @@ -53,11 +50,11 @@ const sidebars = { 'architecture/smart-rollups', // 'architecture/data-availability', // TODO { - type: "category", - label: "Governance", + type: 'category', + label: 'Governance', link: { id: 'architecture/governance', - type: "doc", + type: 'doc', }, items: [ 'architecture/governance/amendment-history', @@ -118,7 +115,7 @@ const sidebars = { label: 'Smart contracts', link: { id: 'smart-contracts', - type: 'doc' + type: 'doc', }, items: [ // 'smart-contracts/quickstart', // TODO @@ -283,6 +280,22 @@ const sidebars = { 'tutorials/build-your-first-app/getting-information', ], }, + + { + type: 'category', + label: 'Start with a minimum dApp and add new features', + link: { + type: 'doc', + id: 'tutorials/dapp', + }, + items: [ + 'tutorials/dapp/part-1', + 'tutorials/dapp/part-2', + 'tutorials/dapp/part-3', + 'tutorials/dapp/part-4', + ], + }, + { type: 'category', label: 'Deploy a smart rollup', @@ -313,8 +326,8 @@ const sidebars = { ], }, ], - } + }, ], -} +}; module.exports = sidebars; diff --git a/static/img/tutorials/dapp-deployedcontracts.png b/static/img/tutorials/dapp-deployedcontracts.png new file mode 100644 index 000000000..514c81064 Binary files /dev/null and b/static/img/tutorials/dapp-deployedcontracts.png differ diff --git a/static/img/tutorials/dapp-kukaifail.png b/static/img/tutorials/dapp-kukaifail.png new file mode 100644 index 000000000..039b04cb8 Binary files /dev/null and b/static/img/tutorials/dapp-kukaifail.png differ diff --git a/static/img/tutorials/dapp-logged.png b/static/img/tutorials/dapp-logged.png new file mode 100644 index 000000000..1b71e0e44 Binary files /dev/null and b/static/img/tutorials/dapp-logged.png differ diff --git a/static/img/tutorials/dapp-mint.png b/static/img/tutorials/dapp-mint.png new file mode 100644 index 000000000..8d0f36559 Binary files /dev/null and b/static/img/tutorials/dapp-mint.png differ diff --git a/static/img/tutorials/dapp-pokecontracts.png b/static/img/tutorials/dapp-pokecontracts.png new file mode 100644 index 000000000..824189d81 Binary files /dev/null and b/static/img/tutorials/dapp-pokecontracts.png differ diff --git a/static/img/tutorials/dapp-pokefail.png b/static/img/tutorials/dapp-pokefail.png new file mode 100644 index 000000000..43aa7ebdc Binary files /dev/null and b/static/img/tutorials/dapp-pokefail.png differ diff --git a/static/img/tutorials/dapp-result.png b/static/img/tutorials/dapp-result.png new file mode 100644 index 000000000..ae6d28246 Binary files /dev/null and b/static/img/tutorials/dapp-result.png differ diff --git a/static/img/tutorials/dapp-success.png b/static/img/tutorials/dapp-success.png new file mode 100644 index 000000000..2d39312ac Binary files /dev/null and b/static/img/tutorials/dapp-success.png differ diff --git a/static/img/tutorials/dapp-table.png b/static/img/tutorials/dapp-table.png new file mode 100644 index 000000000..4fc26ac76 Binary files /dev/null and b/static/img/tutorials/dapp-table.png differ