diff --git a/docs/tutorials/smartpy-fa2-fungible.md b/docs/tutorials/smartpy-fa2-fungible.md new file mode 100644 index 000000000..a16643ab9 --- /dev/null +++ b/docs/tutorials/smartpy-fa2-fungible.md @@ -0,0 +1,65 @@ +--- +title: Create a fungible token with the SmartPy FA2 library +authors: Tim McMackin +last_update: + date: 10 May 2024 +--- + +This tutorial shows you how to use SmartPy's FA2 library to create standards-compliant tokens. + +In this tutorial you will learn: + +- What a fungible token is and how its contract works +- What the FA2 standard is and why token standards are important +- How to use the SmartPy FA2 library to create token contracts +- How to use a local sandbox to test contracts +- How to write custom token behaviors +- How to deploy the contract +- How to interact with tokens directly in wallet apps + +## Prerequisites + +To run this tutorial, you should have a basic understanding of how Tezos works, what blockchain tokens are, and the ability to use the command-line terminal on your computer. + +If you haven't worked with smart contracts before, start with the tutorial https://docs.tezos.com/tutorials/smart-contract. + +## What is a fungible token? + +Fungible tokens are collections of identical, interchangeable tokens, just like one US dollar or Euro is the same as any other US dollar or Euro. +Any number of different accounts can each have a quantity of a certain fungible token. + +By contrast, non-fungible tokens are unique and not interchangeable. +Therefore, only one account can own a specific NFT at one time. + +The term "NFT" is often misused when assets are represented on blockchains, and is often confused with a fungible token. +Make sure that you understand the difference. + +For more information about types of tokens, see [Tokens](../architecture/tokens). + +## What is the FA2 standard? + +FA2 is a standard interface for tokens on Tezos. +It supports several different token types, including fungible and non-fungible tokens. + +Adhering to the FA2 standard allows developers to create new types of tokens while ensuring that the tokens work with existing wallets and applications. +The FA2 standard leaves enough freedom for developers to define rules for transferring tokens and for how tokens behave. + +For more information about FA2 tokens, see [FA2 tokens](../architecture/tokens/FA2). + +## What is the SmartPy FA2 library? + +The SmartPy FA2 library provides classes and other tools to simplify creating FA2-compliant tokens in the SmartPy language. +You create a contract that inherits from a base class in the library to select a kind of token (fungible, single-asset, or non-fungible). +Then you inherit mixins to customize how the token works. + +For more information about the SmartPy FA2 library, see [FA2 lib](https://smartpy.io/guides/FA2-lib/overview) in the SmartPy documentation. + +## Tutorial application + +In this tutorial, you use the SmartPy FA2 library to create a contract that manages multiple fungible tokens. +You add automated tests to the token, deploy it to a local sandbox, customize it further, and deploy it to a test network. +Finally, you work with the token in your own Tezos wallet. + +The completed smart contracts are available here: https://github.com/trilitech/tutorial-applications/tree/main/smartpy_fa2_fungible + +When you're ready, move to the next section to start setting up your token. diff --git a/docs/tutorials/smartpy-fa2-fungible/adding-metadata.md b/docs/tutorials/smartpy-fa2-fungible/adding-metadata.md new file mode 100644 index 000000000..be51ada88 --- /dev/null +++ b/docs/tutorials/smartpy-fa2-fungible/adding-metadata.md @@ -0,0 +1,145 @@ +--- +title: "Part 3: Adding metadata" +authors: Tim McMackin +last_update: + date: 10 May 2024 +--- + +In this part, you configure the metadata for the contract. + +Contract metadata provides details about the contract itself, including its name, description, and what standards it follows. +This information lets off-chain applications like wallets and block explorers show information about the contract. +Contracts can also store other information in metadata, including the code for off-chain views and information about error messages. + +Contracts that manage tokens also store metadata about those tokens, including descriptions of the tokens and links to preview media. + +Generally, contracts store their metadata and token metadata off-chain and include only a link to it in their storage. +Storing the metadata off-chain saves space and makes it easier for off-chain applications to access it. + +## Storing metadata with IPFS + +Many contracts store metadata with the InterPlanetary File System (IPFS) protocol. +This protocol stores files in a decentralized peer-to-peer network and indexes them by their hash. +That way, users can access media by its hash, and the hash allows them to verify that the files have not changed. +As long as one IPFS user has a copy of the data, they can re-upload it to IPFS with the same hash so it is seamlessly available again. + +Therefore, uploading data to IPFS doesn't mean that it will be available forever; at least one user must keep a copy of it. +Instructing an IPFS node to keep a copy of a file is called _pinning_ the file. +SmartPy provides a function that uploads data to IPFS via the Pinata service and instructs Pinata to pin it. + +## Tutorial contract + +The completed contract that you create in this part is at [part_3_complete.py](https://github.com/trilitech/tutorial-applications/blob/main/smartpy_fa2_fungible/part_3_complete.py). + +## Getting a Pinata API key + +SmartPy test scenarios can upload files to IPFS with [Pinata](https://www.pinata.cloud/), so to follow this part of the tutorial, you need a free account on https://www.pinata.cloud and an API key. + +Follow these steps to create a Pinata API key: + +1. Create a free Pinata account at https://app.pinata.cloud/developers/api-keys. + +1. Go to the API Keys tab and click **New Key**. + +1. On the Create New API Key page, expand API Endpoint Access and enable the `pinFileToIPFS` permission, as in this picture: + + Selecting the permissions for the Pinata key + +1. In the **Key Name** field, give the key a name, such as "My Key." + +1. Click **Create Key**. + + The API Key Info window shows the API key and secret, which you must copy immediately, because it is not shown again. + +1. Copy the API Key and API Secret fields and save the values on your computer. +You need these values in the next section. + + You can see the new API key on the API Keys tab: + + ![The new Pinata API key in the Pinata web app](/img/tutorials/created-pinata-key.png) + +## Uploading metadata + +The SmartPy `sp.pin_on_ipfs` function uploads metadata to IPFS via your Pinata account and "pins" it to ensure that it remains available until you remove it. +In this section, you add code to use this function to upload and pin contract metadata to IPFS when the test scenario runs. +It is still advisable to save a local copy of your metadata, just as you would with any upload service. + +1. After the code that instantiates the contract in the test scenario, use the `sp.create_tzip16_metadata` function to create a metadata object that is compatible with the [TZIP-16](https://gitlab.com/tezos/tzip/-/blob/master/proposals/tzip-16/tzip-16.md) standard for contract metadata: + + ```smartpy + # Build contract metadata content + contract_metadata = sp.create_tzip16_metadata( + name="My FA2 fungible token contract", + description="This is an FA2 fungible token contract using SmartPy.", + version="1.0.0", + license_name="CC-BY-SA", + license_details="Creative Commons Attribution Share Alike license 4.0 https://creativecommons.org/licenses/by/4.0/", + interfaces=["TZIP-012", "TZIP-016"], + authors=["SmartPy "], + homepage="https://smartpy.io/ide?template=fa2_lib_fungible.py", + # Optionally, upload the source code to IPFS and add the URI here + source_uri=None, + offchain_views=contract.get_offchain_views(), + ) + ``` + + This code must come after you create the contract because it uses the `contract.get_offchain_views` function to retrieve the contract's off-chain views. + Off-chain views are stored in metadata, not in the contract code or storage. + +1. Optional: Edit the metadata fields with information about your contract. + +1. Add permission information to the metadata with this code: + + ```smartpy + # Add the info specific to FA2 permissions + contract_metadata["permissions"] = { + # The operator policy chosen: + # owner-or-operator-transfer is the default. + "operator": "owner-or-operator-transfer", + # Those two options should always have these values. + # It means that the contract doesn't use the hook mechanism. + "receiver": "owner-no-hook", + "sender": "owner-no-hook", + } + ``` + + This metadata describes who is allowed to transfer tokens. + The permissions in this code allow token owners and operators to transfer tokens. + For more information about operators, see [FA2 tokens](../../architecture/tokens/FA2). + +1. Pin the metadata to IPFS and get its URI with this code: + + ```smartpy + # Upload the metadata to IPFS and get its URI + metadata_uri = sp.pin_on_ipfs(contract_metadata, api_key=None, secret_key=None) + ``` + +1. Update the `None` values in the `sp.pin_on_ipfs` function with your Pinata API key and secret key. +You can also put them in the `PINATA_KEY` and `PINATA_SECRET` environment variables and remove the `api_key` and `secret_key` parameters from the function call. + +1. Create the contract metadata and add it to the contract with this code: + + ```smartpy + # Create the metadata big map based on the IPFS URI + contract_metadata = sp.scenario_utils.metadata_of_url(metadata_uri) + + # Update the scenario instance with the new metadata + contract.data.metadata = contract_metadata + ``` + + This code creates a big map that has a single entry that points to the metadata URI. + +1. Run the `python fa2_fungible.py` command to compile and test your contract. +Now, SmartPy uploads the metadata as part of the compilation process. + +1. In your Pinata account, verify that the metadata file uploaded correctly. +By default, the file is named "No name set" and looks like this in your Pinata files: + + ![The uploaded file on Pinata](/img/tutorials/pinata-pinned-file.png) + + You can click this file to see it and verify that the name and description at the top of the file match the name and description of your contract. + The rest of the file is the code of off-chain views that the FA2 library adds automatically. + +The completed contract is at [part_3_complete.py](https://github.com/trilitech/tutorial-applications/blob/main/smartpy_fa2_fungible/part_3_complete.py). + +Now when you deploy the completed contract, wallets and block explorers can show information about it. diff --git a/docs/tutorials/smartpy-fa2-fungible/basic-fa2-token.md b/docs/tutorials/smartpy-fa2-fungible/basic-fa2-token.md new file mode 100644 index 000000000..75b0f8044 --- /dev/null +++ b/docs/tutorials/smartpy-fa2-fungible/basic-fa2-token.md @@ -0,0 +1,520 @@ +--- +title: "Part 1: Setting up a simple FA2 token" +authors: Tim McMackin +last_update: + date: 22 April 2024 +--- + +In the first part of this tutorial, you create an FA2 token contact that has only the basic features that the standard requires. +For example, the standard does not require the contract to have `mint` and `burn` entrypoints that allow administrators to create and destroy tokens. +In this case, you create the contract with all of the tokens that it will ever have. + +## Prerequisites + +To run this part of the tutorial, makes sure that you have the following tools installed: + +- [Python](https://www.python.org/) and the `pip` package manager +- [SmartPy](https://smartpy.io/manual/introduction/installation) + +## Tutorial contract + +The completed contract that you create in this part is at [part_1_complete.py](https://github.com/trilitech/tutorial-applications/blob/main/smartpy_fa2_fungible/part_1_complete.py). + +## Using the library to create a contract + +The FA2 library provides classes that you can extend to create your contract class. +Each class creates a certain type of token contract: + +- `main.Nft`: Non-fungible tokens, which are unique digital assets +- `main.Fungible`: Fungible tokens, which are interchangeable assets, like tez or other cryptocurrencies +- `main.SingleAsset`: Single-asset tokens, which are a simplified case of fungible tokens, allowing only one token type per contract + +Follow these steps to create your own token contract based on the `main.Fungible` base class: + +1. Create a Python file with a `.py` extension, such as `fa2_fungible.py`, in any text editor. + +1. In the file, import SmartPy and its FA2 modules: + + ```smartpy + import smartpy as sp + from smartpy.templates import fa2_lib as fa2 + + # Alias the main template for FA2 contracts + main = fa2.main + ``` + +1. Create a SmartPy module to store your contract class: + + ```smartpy + @sp.module + def my_module(): + ``` + +1. In the module, create a contract class that inherits from the base class and the `OnchainviewBalanceOf` class, which provides an on-chain view that provides token balances: + + ```smartpy + class MyFungibleContract( + main.Fungible, + main.OnchainviewBalanceOf, + ): + ``` + +1. Create the contract's `__init__()` method and initialize the superclasses: + + ```smartpy + def __init__(self, contract_metadata, ledger, token_metadata): + + # Initialize on-chain balance view + main.OnchainviewBalanceOf.__init__(self) + + # Initialize fungible token base class + main.Fungible.__init__(self, contract_metadata, ledger, token_metadata) + ``` + + Note the order of these classes both in the `class` statement and within the `__init__()` method. + You must inherit and initialize these classes in a specific order for them to work, as described in [Mixins](https://smartpy.io/guides/FA2-lib/mixins). + +1. Outside the module, add these two utility functions to call views in the contract: + + ```smartpy + def _get_balance(fa2_contract, args): + """Utility function to call the contract's get_balance view to get an account's token balance.""" + return sp.View(fa2_contract, "get_balance")(args) + + + def _total_supply(fa2_contract, args): + """Utility function to call the contract's total_supply view to get the total amount of tokens.""" + return sp.View(fa2_contract, "total_supply")(args) + ``` + +At this point, the contract looks like this: + +```smartpy +import smartpy as sp +from smartpy.templates import fa2_lib as fa2 + +# Alias the main template for FA2 contracts +main = fa2.main + + +@sp.module +def my_module(): + class MyFungibleContract( + main.Fungible, + main.OnchainviewBalanceOf, + ): + def __init__(self, contract_metadata, ledger, token_metadata): + + # Initialize on-chain balance view + main.OnchainviewBalanceOf.__init__(self) + + # Initialize fungible token base class + main.Fungible.__init__(self, contract_metadata, ledger, token_metadata) + + +def _get_balance(fa2_contract, args): + """Utility function to call the contract's get_balance view to get an account's token balance.""" + return sp.View(fa2_contract, "get_balance")(args) + + +def _total_supply(fa2_contract, args): + """Utility function to call the contract's total_supply view to get the total amount of tokens.""" + return sp.View(fa2_contract, "total_supply")(args) +``` + +Indentation is significant in Python, so make sure that your contract is indented like this example. + +This short contract is all of the necessary code for a basic FA2 token contract. +The inherited classes provide all of the necessary entrypoints. + +## Adding the contract to a test scenario + +SmartPy provides a testing framework that runs contracts in a realistic simulation called a test scenario. +This test scenario is also the way SmartPy compiles contracts to Michelson for deployment, so you must add your contract to a test scenario. + +1. At the end of the contract file, define a test scenario function with this code: + + ```smartpy + @sp.add_test() + def test(): + ``` + + The code within this scenario is ordinary Python, not SmartPy, so you can do all of the things that you can normally do in Python, including importing modules and calling external APIs. + Code within a SmartPy module (annotated with `@sp.module`) is SmartPy code and is limited by what you can do in smart contracts. + Importing modules and calling external APIs isn't possible in SmartPy modules, except for the modules that SmartPy provides. + +1. Inside the test scenario function, initialize the test scenario by passing a name for the scenario and the modules to use in the scenario to the `sp.test_scenario` function: + + ```smartpy + # Create and configure the test scenario + # Import the types from the FA2 library, the library itself, and the contract module, in that order. + scenario = sp.test_scenario("fa2_lib_fungible", [fa2.t, fa2.main, my_module]) + ``` + +1. Define test accounts to use in the scenario with the `sp.test_account` function: + + ```smartpy + # Define test accounts + alice = sp.test_account("Alice") + bob = sp.test_account("Bob") + ``` + +1. Set up token metadata, which describes the tokens to users. +For example, these lines create metadata for two token types, named `Token Zero` and `Token One`: + + ```smartpy + # Define initial token metadata + tok0_md = fa2.make_metadata(name="Token Zero", decimals=0, symbol="Tok0") + tok1_md = fa2.make_metadata(name="Token One", decimals=0, symbol="Tok1") + ``` + + The `fa2.make_metadata` function creates a token metadata object that complies with the [TZIP-12](https://gitlab.com/tezos/tzip/-/blob/master/proposals/tzip-12/tzip-12.md) standard. + + Your contract can have as many types of fungible tokens as you want, but the examples in this tutorial use these two token types. + The contract assigns them numeric IDs starting at 0. + +1. Create the starting ledger for the contract, which lists the tokens and their owners. +This example gives 10 of token 0 to the Alice test account and 10 of token 1 to the Bob test account: + + ```smartpy + # Define tokens and initial owners + initial_ledger = { + (alice.address, 0): 10, + (bob.address, 1): 10, + } + ``` + + The type of this ledger is set by the FA2 standard, so casting it to the `ledger_fungible` type helps you make sure that your ledger matches the standard. + + The ledger is a big map where the key is a pair of the token owner and token ID and the value is the amount of tokens. + In table format, the ledger looks like this: + + key | value + --- | --- + Alice, token ID 0 | 10 + Bob, token ID 1 | 10 + + You can distribute tokens to any accounts that you want in the test scenario. + Because the contract has no `mint` entrypoint, this initial ledger defines all of the tokens and token types that the contract contains. + +1. Create an instance of the contract in the test scenario and pass the values for its initial storage: + + ```smartpy + # Instantiate the FA2 fungible token contract + contract = my_module.MyFungibleContract(sp.big_map(), initial_ledger, [tok0_md, tok1_md]) + ``` + + These are the parameters for the contract's `__init__()` method: + + - The contract metadata, which is blank for now + - The initial ledger + - The metadata for the token types, in a list + +1. Add the contract to the test scenario, which deploys (originates) it to the simulated Tezos environment: + + ```smartpy + # Originate the contract in the test scenario + scenario += contract + `````` + +At this point, the contract looks like this: + +```smartpy +import smartpy as sp +from smartpy.templates import fa2_lib as fa2 + +# Alias the main template for FA2 contracts +main = fa2.main + + +@sp.module +def my_module(): + class MyFungibleContract( + main.Fungible, + main.OnchainviewBalanceOf, + ): + def __init__(self, contract_metadata, ledger, token_metadata): + + # Initialize on-chain balance view + main.OnchainviewBalanceOf.__init__(self) + + # Initialize fungible token base class + main.Fungible.__init__(self, contract_metadata, ledger, token_metadata) + + +def _get_balance(fa2_contract, args): + """Utility function to call the contract's get_balance view to get an account's token balance.""" + return sp.View(fa2_contract, "get_balance")(args) + + +def _total_supply(fa2_contract, args): + """Utility function to call the contract's total_supply view to get the total amount of tokens.""" + return sp.View(fa2_contract, "total_supply")(args) + + +@sp.add_test() +def test(): + # Create and configure the test scenario + # Import the types from the FA2 library, the library itself, and the contract module, in that order. + scenario = sp.test_scenario("fa2_lib_fungible", [fa2.t, fa2.main, my_module]) + + # Define test accounts + alice = sp.test_account("Alice") + bob = sp.test_account("Bob") + + # Define initial token metadata + tok0_md = fa2.make_metadata(name="Token Zero", decimals=0, symbol="Tok0") + tok1_md = fa2.make_metadata(name="Token One", decimals=0, symbol="Tok1") + + # Define tokens and initial owners + initial_ledger = { + (alice.address, 0): 10, + (bob.address, 1): 10, + } + + # Instantiate the FA2 fungible token contract + contract = my_module.MyFungibleContract(sp.big_map(), initial_ledger, [tok0_md, tok1_md]) + + # Originate the contract in the test scenario + scenario += contract +``` + +## Adding tests + +SmartPy has built-in tools for testing contracts as part of the test scenario. +In test scenarios, you can call contract entrypoints, verify the contract storage, and do other things to make sure that the contract is working as expected. + +Follow these steps to add tests to the contract: + +1. At the end of the file, add this code to verify the initial state of the ledger: + + ```smartpy + scenario.h2("Verify the initial owners of the tokens") + scenario.verify( + _get_balance(contract, sp.record(owner=alice.address, token_id=0)) == 10 + ) + scenario.verify( + _get_balance(contract, sp.record(owner=bob.address, token_id=0)) == 0 + ) + scenario.verify( + _get_balance(contract, sp.record(owner=alice.address, token_id=1)) == 0 + ) + scenario.verify( + _get_balance(contract, sp.record(owner=bob.address, token_id=1)) == 10 + ) + scenario.verify(_total_supply(contract, sp.record(token_id=0)) == 10) + scenario.verify(_total_supply(contract, sp.record(token_id=1)) == 10) + ``` + + This code uses the `get_balance_of` and `total_supply` views to check the current owners of the tokens and the total amount of tokens when the contract is deployed. + + Note that the calls to these views are within the `scenario.verify` function. + To verify details about the deployed contract, you must use this function to access the state of the contract within the test scenario. + +1. Add this code to call the contract's `transfer` entrypoint and verify that the tokens transferred correctly: + + ```smartpy + scenario.h2("Transfer tokens") + # Bob sends 3 of token 1 to Alice + contract.transfer( + [ + sp.record( + from_=bob.address, + txs=[sp.record(to_=alice.address, amount=3, token_id=1)], + ), + ], + _sender=bob, + ) + scenario.verify( + _get_balance(contract, sp.record(owner=alice.address, token_id=0)) == 10 + ) + scenario.verify( + _get_balance(contract, sp.record(owner=bob.address, token_id=0)) == 0 + ) + scenario.verify( + _get_balance(contract, sp.record(owner=alice.address, token_id=1)) == 3 + ) + scenario.verify( + _get_balance(contract, sp.record(owner=bob.address, token_id=1)) == 7 + ) + scenario.verify(_total_supply(contract, sp.record(token_id=0)) == 10) + scenario.verify(_total_supply(contract, sp.record(token_id=1)) == 10) + + # Alice sends 4 of token 0 to Bob + contract.transfer( + [ + sp.record( + from_=alice.address, + txs=[sp.record(to_=bob.address, amount=4, token_id=0)], + ), + ], + _sender=alice, + ) + scenario.verify( + _get_balance(contract, sp.record(owner=alice.address, token_id=0)) == 6 + ) + scenario.verify( + _get_balance(contract, sp.record(owner=bob.address, token_id=0)) == 4 + ) + scenario.verify( + _get_balance(contract, sp.record(owner=alice.address, token_id=1)) == 3 + ) + scenario.verify( + _get_balance(contract, sp.record(owner=bob.address, token_id=1)) == 7 + ) + scenario.verify(_total_supply(contract, sp.record(token_id=0)) == 10) + scenario.verify(_total_supply(contract, sp.record(token_id=1)) == 10) + ``` + + This code calls the transfer entrypoint, passes the transfer information, and adds the `_sender=bob` parameter to indicate that the transfer came from Bob's account. + Then it calls the contract's views again to verify that the tokens were transferred and that the total supply of tokens remains the same. + Then it tries again from Alice's account. + +1. Test an entrypoint call that should fail by adding this code: + + ```smartpy + # Bob cannot transfer Alice's tokens + contract.transfer( + [ + sp.record( + from_=alice.address, + txs=[sp.record(to_=bob.address, amount=1, token_id=0)], + ), + ], + _sender=bob, + _valid=False, + ) + ``` + + This call should fail because it comes from Bob's account but the call tries to transfer tokens out of Alice's account. + The call includes the `_valid=False` parameter to indicate that this call should fail. + +You can add any number of tests to your test scenario. +In practice, you should test all features of your contract thoroughly to identify any problems before deployment. + +## Compiling the contract + +To compile the contract, use the `python` command, just like any other Python file: + +```bash +python fa2_fungible.py +``` + +If SmartPy compiles your contract successfully, nothing is printed to the command line output. +Its compiler writes your contract to a folder with the name in the `sp.test_scenario` function, which is `fa2_lib_fungible` in this example. +This folder has many files, including: + +- `log.txt`: A compilation log that lists the steps in the test scenario and connects them to the other files that the compiler created. +- `step_003_cont_0_contract.tz`: The compiled Michelson contract, which is what you deploy to Tezos in the next section. +The compiler also creates a JSON version of the compiled contract. +- `step_003_cont_0_storage.tz`: The compiled Michelson value of the initial contract storage, based on the parameters you passed when you instantiated the contract in the test scenario. +The compiler also creates a JSON and Python version of the storage. +- `step_003_cont_0_types.py`: The Python types that the contract uses, which can help you call the contract from other SmartPy contracts. + +You can use the compiled contract and storage files to deploy the contract. +In the next section, you deploy the contract to a local Tezos sandbox. + +## Troubleshooting + +If the `python` command shows any errors, make sure that your contract matches the example. +In particular, check your indentation, because indentation is significant in Python and SmartPy. + +You can compare your contract with the completed contract here: [part_1_complete.py](https://github.com/trilitech/tutorial-applications/blob/main/smartpy_fa2_fungible/part_1_complete.py). + + + +## (Optional) Deploy the contract to the Octez client mockup mode + +The Octez client provides a few local sandbox modes that you can use to test contracts without uploading them to a test network or running a local Tezos environment. + +Follow these steps to set up the Octez client mockup mode and deploy the contract to it: + +1. Install the Octez client by following the steps in [Installing the Octez client](../../developing/octez-client/installing). + +1. Set up the Octez client mockup mode: + + 1. Run this command to start mockup mode: + + ```bash + octez-client \ + --protocol ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK \ + --base-dir /tmp/mockup \ + --mode mockup \ + create mockup + ``` + + Now you can run commands in mockup mode by prefixing them with `octez-client --mode mockup --base-dir /tmp/mockup`. + + 1. Create an alias for mockup mode: + + ```bash + alias mockup-client='octez-client --mode mockup --base-dir /tmp/mockup' + ``` + + Now you can run commands in mockup mode with the `mockup-client` alias, as in this example: + + ```bash + mockup-client list known addresses + ``` + +1. Deploy the contract to mockup mode: + + 1. Create two Octez accounts to represent the Alice and Bob accounts in the test scenario: + + ```bash + mockup-client gen keys alice + mockup-client gen keys bob + ``` + + 1. Get the addresses for Alice and Bob by running this command: + + ```bash + mockup-client list known addresses + ``` + + 1. Replace the addresses in the initial storage value in the `step_003_cont_0_storage.tz` file with the mockup addresses. + For example, the file might look like this: + + ```michelson + (Pair {Elt (Pair "tz1Utg2AKcbLgVokY7J8QiCjcfo5KHk3VtHU" 1) 10; Elt (Pair "tz1XeaZgLqgCqMA3Egz5Q7bqiqdFefaNoQd5" 0) 10} (Pair {} (Pair 2 (Pair {} (Pair {Elt 0 10; Elt 1 10} {Elt 0 (Pair 0 {Elt "decimals" 0x30; Elt "name" 0x546f6b656e205a65726f; Elt "symbol" 0x546f6b30}); Elt 1 (Pair 1 {Elt "decimals" 0x30; Elt "name" 0x546f6b656e204f6e65; Elt "symbol" 0x546f6b31})}))))) + ``` + + Now you can use this file as the initial storage value and give the initial tokens to the mockup addresses. + + 1. Deploy the contract by passing the compiled contract and initial storage value to the `originate contract` command. + For example, if your compiled files are in the `fa2_lib_fungible` folder, the command looks like this: + + ```bash + mockup-client originate contract smartpy_fa2_fungible \ + transferring 0 from bootstrap1 \ + running fa2_lib_fungible/step_003_cont_0_contract.tz \ + --init "$(cat fa2_lib_fungible/step_003_cont_0_storage.tz)" --burn-cap 3 --force + ``` + + If you see errors that refer to unexpected characters, make sure the paths to the files are correct and that you changed only the content of addresses inside quotes in the storage file. + + If you see the error "Keys in a map literal must be in strictly ascending order, but they were unordered in literal," reverse the order of the two addresses. + + If the deployment succeeds, the Octez client prints the address of the contract and aliases it as `smartpy_fa2_fungible`. + + 1. Use the built-in `get_balance_of` view to see the tokens that one of the accounts owns: + + ```bash + mockup-client run view get_balance_of \ + on contract smartpy_fa2_fungible \ + with input '{Pair "tz1Utg2AKcbLgVokY7J8QiCjcfo5KHk3VtHU" 1}' + ``` + + The response shows a Michelson value that includes the ID and amount of tokens that the address owns, as in this example: + + ```michelson + { Pair (Pair "tz1Utg2AKcbLgVokY7J8QiCjcfo5KHk3VtHU" 1) 10 } + ``` + + + +For more information, see [Mockup mode](https://tezos.gitlab.io/user/mockup.html) in the Octez documentation. + +Now you have a basic FA2 fungible token contract that starts with a predefined amount of tokens. + +In the next part, you add minting and burning functionality to the contract so an administrator can create and destroy tokens. diff --git a/docs/tutorials/smartpy-fa2-fungible/customizing-operations.md b/docs/tutorials/smartpy-fa2-fungible/customizing-operations.md new file mode 100644 index 000000000..c36c985c3 --- /dev/null +++ b/docs/tutorials/smartpy-fa2-fungible/customizing-operations.md @@ -0,0 +1,238 @@ +--- +title: "Part 4: Customizing operations" +authors: Tim McMackin +last_update: + date: 10 May 2024 +--- + +As shown in previous parts, the SmartPy FA2 library provides the entrypoints that the standard requires. +You can override these entrypoints, but you must be sure to follow the standard. +You can also customize their behavior by setting security policies. + + +You can also customize the contract by adding your own entrypoints. +In this part, you add an entrypoint that allows users to exchange one token for another. +To convert one token into another, the entrypoint follows these general steps: + +1. Verify that the source and target tokens are defined. +1. Verify that the requester has permission to access the source token. +1. Burn the source tokens by decreasing the amount in the ledger for the account or fail if the account doesn't have enough. +1. Mint the target tokens by increasing the amount in the ledger for the account. + +The burn and mint steps are straightforward as long as you understand how FA2 contracts store information in their ledger. +As described in [Part 1](./basic-fa2-token), the contract stores information about who owns tokens in a key-value store: + +- The key is a pair that contains the address of the owner and the ID of the token +- The value is the quantity of tokens + +In table format, the ledger might look like this after some mint and burn transactions: + +key | value +--- | --- +Alice, token ID 0 | 10 +Alice, token ID 1 | 2 +Alice, token ID 2 | 1 +Alice, token ID 4 | 5 +Bob, token ID 1 | 2 +Bob, token ID 2 | 8 +Bob, token ID 3 | 14 + +That means that to get the amount of the source token that an account has, you must put the address and token ID together as a pair. + +## Tutorial contract + +The completed contract that you create in this part is at [part_4_complete.py](https://github.com/trilitech/tutorial-applications/blob/main/smartpy_fa2_fungible/part_4_complete.py). + +## Exchanging one token for another + +Follow these steps to create the `convert` entrypoint that exchanges one token for another: + +1. At the beginning of the module, after the `def my_module():` statement but before the `class` statement, add a type that represents the information for a token transfer: + + ```smartpy + conversion_type: type = sp.record( + from_ = sp.address, # Account to convert tokens from + source_token_id = sp.nat, # The ID of the source token + target_token_id = sp.nat, # The ID of the target token + amount = sp.nat, # The number of source tokens to convert + ) + ``` + + The type includes the account, the ID of the source token, the ID of the token to convert it into, and the amount of tokens to convert. + +1. After this type, create a type that is a list of conversions: + + ```smartpy + conversion_batch: type = sp.list[conversion_type] + ``` + + This type is the parameter for the `convert` entrypoint. + The FA2 standard says that custom entrypoints should accept batches for parameters to allow users to do multiple things in a single transaction. + +1. After the `__init__()` function, add an entrypoint with the `@sp.entrypoint` annotation and accept a parameter of the `conversion_match` type: + + ```smartpy + # Convert one token into another + @sp.entrypoint + def convert(self, batch): + sp.cast(batch, conversion_batch) + ``` + +1. Loop over the conversions in the batch: + + ```smartpy + for conversion in batch: + ``` + +1. Within the loop, destructure the conversion into individual variables: + + ```smartpy + record( + from_, + source_token_id, + target_token_id, + amount + ).match = conversion + ``` + +1. Add this code to verify that the contract's security policy allows transfers (which it does by default) and that the source and target token exist: + + ```smartpy + # Verify that transfers are allowed + assert self.private.policy.supports_transfer, "FA2_TX_DENIED" + # Verify that the source and target tokens exist + assert self.is_defined_(source_token_id), "FA2_TOKEN_UNDEFINED" + assert self.is_defined_(target_token_id), "FA2_TOKEN_UNDEFINED" + ``` + +1. Add this code to verify that the account that sent the request is either the owner or an operator of the tokens: + + ```smartpy + # Verify that the sender is either the owner or the operator of the source tokens + assert (sp.sender == from_) or self.data.operators.contains( + sp.record( + owner=from_, operator=sp.sender, token_id=source_token_id + ) + ), "FA2_NOT_OPERATOR" + ``` + + In FA2 contracts, an operator is an account that is authorized to transfer tokens that are owned by another account. + For more information, see [Operators](../../architecture/tokens/FA2). + + Note that this code uses `sp.sender` instead of `sp.source` to identify the account that sent the transaction. + The source is the account that initiated the original transaction that led to this entrypoint call, while the sender is the account that made the call that led directly to this entrypoint call. + Using sender here is important to prevent other contracts from accepting a transaction from an account and then sending other transactions that impersonate that account. + For more information, see [Avoiding flaws](https://opentezos.com/smart-contracts/avoiding-flaws) on opentezos.com. + +1. Create a pair that represents the key for the source token type: + + ```smartpy + # Get a pair to represent the key for the ledger for the source tokens + from_source = (from_, source_token_id) + ``` + +1. Add this code to burn the source tokens: + + ```smartpy + # Burn the source tokens + self.data.ledger[from_source] = sp.as_nat( + self.data.ledger.get(from_source, default=0) - amount, + error="FA2_INSUFFICIENT_BALANCE", + ) + is_supply = sp.is_nat( + self.data.supply.get(source_token_id, default=0) - amount + ) + with sp.match(is_supply): + with sp.case.Some as supply: + self.data.supply[source_token_id] = supply + with None: + self.data.supply[source_token_id] = 0 + ``` + + This code uses the key from the previous step to subtract the tokens from the ledger entry. + Then it updates the contract storage with the total number of that type of token remaining. + +1. Add this code to create a pair that represents the key for the target token type: + + ```smartpy + # Get a pair to represent the key for the ledger for the target tokens + from_target = (from_, target_token_id) + ``` + +1. Add this code to mint the target tokens: + + ```smartpy + # Mint the target tokens + target_amount = self.data.ledger.get(from_target, default=0) + self.data.ledger[from_target] = amount + target_amount + self.data.supply[target_token_id] += amount + ``` + + This code attempts to retrieve the record by the pair in the previous step. + If it exists, the code adds the number of tokens to the existing amount. + If not, it creates a new record in the ledger. + Finally, it increases the supply of that token. + +1. At the end of the file, add this test to verify that a user can convert their own tokens but not other accounts' tokens: + + ```smartpy + scenario.h2("Convert tokens") + + # Verify that you can convert your own tokens + conversions = [ + sp.record(source_token_id = 0, target_token_id = 1, amount = 2), + ] + contract.convert( + conversions, + _sender=alice + ) + scenario.verify( + _get_balance(contract, sp.record(owner=alice.address, token_id=0)) == 8 + ) + scenario.verify( + _get_balance(contract, sp.record(owner=alice.address, token_id=1)) == 5 + ) + scenario.verify(_total_supply(contract, sp.record(token_id=0)) == 12) + scenario.verify(_total_supply(contract, sp.record(token_id=1)) == 16) + + # Verify that you can't convert someone else's tokens + contract.convert( + conversions, + _sender=bob, + _valid=False, + _exception="FA2_NOT_OPERATOR" + ) + ``` + +1. Add this test to verify that an account can set another account as an operator and that the operator can transfer their tokens: + + ```smartpy + # Make Bob an operator of Alice's token 0 + operator_update = sp.record(owner=alice.address, operator=bob.address, token_id=0) + contract.update_operators( + [ + sp.variant.add_operator(operator_update) + ], + _sender=alice + ) + # Have Bob convert Alice's tokens + contract.convert( + conversions, + _sender=bob + ) + scenario.verify( + _get_balance(contract, sp.record(owner=alice.address, token_id=0)) == 6 + ) + scenario.verify( + _get_balance(contract, sp.record(owner=alice.address, token_id=1)) == 7 + ) + scenario.verify(_total_supply(contract, sp.record(token_id=0)) == 10) + scenario.verify(_total_supply(contract, sp.record(token_id=1)) == 18) + ``` + +That's all that's necessary to convert one fungible token into another. +If you wanted to extend this feature, you could implement an exchange rate, take a fee for converting tokens, or allow only certain accounts to convert tokens. +You could also test the entrypoint more thoroughly, such as testing that a user can't convert more tokens than they have. + +If you want to, you can deploy this new contract to the mockup mode with the same commands as in [Part 1: Setting up a simple FA2 token](./basic-fa2-token) and try it out locally. +In the next section, you deploy it to a test network. diff --git a/docs/tutorials/smartpy-fa2-fungible/deploying-contract.md b/docs/tutorials/smartpy-fa2-fungible/deploying-contract.md new file mode 100644 index 000000000..4c83f2ceb --- /dev/null +++ b/docs/tutorials/smartpy-fa2-fungible/deploying-contract.md @@ -0,0 +1,114 @@ +--- +title: "Part 5: Deploying the contract" +authors: Tim McMackin +last_update: + date: 22 April 2024 +--- + +So far you have used the token in the SmartPy test scenario and in the Octez client local sandbox. +To test it on a live network, you can use the Ghostnet test network. + +For more information about testnets, see [Testing on sandboxes and testnets](../../developing/testnets). + +## Configuring the Octez client for Ghostnet + +Follow these steps to set up your installation of the Octez client to work with Ghostnet: + +1. Go to https://teztnets.com/ghostnet-about and copy the URL of a public RPC node for Ghostnet, such as https://rpc.ghostnet.teztnets.com. + +1. Use that URL to configure the Octez client in this command: + + ```bash + octez-client -E https://rpc.ghostnet.teztnets.com config update + ``` + +1. If you don't have an account on Ghostnet, create or import one with the instructions in [Creating accounts](../../developing/octez-client/accounts). + +1. Use the [Ghostnet faucet](https://faucet.ghostnet.teztnets.com/) to get some tez for the account. + +## Setting the admin account in the contract + +Currently, the admin account in the contract is an automatically-generated account. +Follow these steps to use your account as the admin account instead: + +1. Compile the contract with the `python` command as you did in previous steps. + +1. Get the address of your account by running this command: + + ```bash + octez-client list known addresses + ``` + +1. Update the `step_003_cont_0_storage.tz` file and replace the first address listed with your account. +This is the field that stores the admin account in the contract storage. + +1. Deploy the contract to Ghostnet by passing your account alias, the compiled contract, and initial storage value to the `originate contract` command. +For example, if your account is named `my_account` and the compiled files are in the `fa2_lib_fungible` folder, the command looks like this: + + ```bash + octez-client originate contract smartpy_fa2_fungible \ + transferring 0 from my_account \ + running fa2_lib_fungible/step_003_cont_0_contract.tz \ + --init "$(cat fa2_lib_fungible/step_003_cont_0_storage.tz)" --burn-cap 3 --force + ``` + +1. Copy the address of the contract from the output of the command. +The contract address starts with `KT1`. + +1. Look up the contract on the block explorer [Better Call Dev](https://better-call.dev/) + + Note that the block explorer recognizes that the contract is FA2-compliant and shows an FA2 icon at the top of the page. + The block explorer also shows information about the tokens on the Tokens tab. + +1. Mint some tokens and send them to your account from the Interact tab. +For example, to mint tokens of an existing token type, click **Add**, select `existing` for the transaction, and specify your address and the number of tokens to create, as in this picture: + + Specifying parameters for the mint transaction + +## Interact with the token in a wallet + +Because the token is FA2-compliant, wallet applications can work with it directly. +However, you must add the token contract to your wallet for it to recognize the token. + +The process for adding a token contract to a wallet depends on the wallet application. +Here are steps for the Temple wallet: + +1. Copy the address of the contract. +You can use the command `octez-client list known contracts` to print the addresses of contracts that the Octez client knows about. + +1. Open the Temple wallet and sign in. + +1. Under the list of tokens, click **Manage assets list**: + + Opening the asset list filter + +1. From the popup window, click **Manage**: + + The Manage button in the popup window + +1. Click **Add Asset**: + + Adding an asset to the tokens list + +1. In the window that opens add information about the token, including the contract address, the token ID (such as 0 or 1), and the the symbol for the token, such as `TOK0`: + + Adding information about the token + +1. Click **Add Asset**. + +Now the token appears in your wallet just like any other token: + +The new token in the Temple wallet + +From here, you can run transactions on the token, such as sending it to a different account. + +## Next steps + +Now that you have an FA2-compliant token, you can use it with all kinds of Tezos dApps. +If you want to continue working with the token, here are some ideas: + +- Implement an exchange rate for the `convert` entrypoint +- Implement other custom behaviors for your token while keeping it FA2-compliant +- Build a front-end application to make it easier to interact with it +- Add other token types and metadata for them +- Try creating a single asset contract or an NFT contract with the SmartPy FA2 library diff --git a/docs/tutorials/smartpy-fa2-fungible/minting-and-burning.md b/docs/tutorials/smartpy-fa2-fungible/minting-and-burning.md new file mode 100644 index 000000000..993acaffb --- /dev/null +++ b/docs/tutorials/smartpy-fa2-fungible/minting-and-burning.md @@ -0,0 +1,225 @@ +--- +title: "Part 2: Adding minting and burning entrypoints" +authors: Tim McMackin +last_update: + date: 22 April 2024 +--- + +In this part, you add entrypoints that allow an administrator account to create tokens and allow users to burn their own tokens. + +The SmartPy FA2 library provides _mixins_ that add these entrypoints so you don't have to code them yourself. +Mixins are modular classes that add specific pieces of functionality. + +## Tutorial contract + +The completed contract that you create in this part is at [part_2_complete.py](https://github.com/trilitech/tutorial-applications/blob/main/smartpy_fa2_fungible/part_2_complete.py). + +## Adding the admin, mint, and burn entrypoints + +To add mint and burn entrypoints to the contract, you need three mixins: the mixins for those two entrypoints and a mixin that enables administrator functionality. +Only the admin account can mint tokens, but anyone can burn their own tokens. + +1. In the contract, update the `class` statement to include the new mixins: + + ```smartpy + # Order of inheritance: [Admin], [], , []. + class MyFungibleContract( + main.Admin, + main.Fungible, + main.MintFungible, + main.BurnFungible, + main.OnchainviewBalanceOf, + ): + ``` + +1. Update the `__init__()` function to accept the admin address and initialize the mixins: + + ```smartpy + def __init__(self, admin_address, contract_metadata, ledger, token_metadata): + + # Initialize on-chain balance view + main.OnchainviewBalanceOf.__init__(self) + + # Initialize the fungible token-specific entrypoints + main.BurnFungible.__init__(self) + main.MintFungible.__init__(self) + + # Initialize fungible token base class + main.Fungible.__init__(self, contract_metadata, ledger, token_metadata) + + # Initialize administrative permissions + main.Admin.__init__(self, admin_address) + ``` + + The order that you import and initialize the mixins is significant, so make sure your updates match the code above. + +1. In the test scenario, add an administrator test account to the existing Alice and Bob test accounts: + + ```smartpy + # Define test accounts + admin = sp.test_account("Admin") + alice = sp.test_account("Alice") + bob = sp.test_account("Bob")``` + +1. Update the command to deploy the contract to include the administrator address: + + ```smartpy + # Instantiate the FA2 fungible token contract + contract = my_module.MyFungibleContract(admin.address, sp.big_map(), initial_ledger, [tok0_md, tok1_md]) + ``` + +1. At the end of the test scenario, add a test to verify that the admin account can mint more of an existing token: + + ```smartpy + scenario.h2("Mint tokens") + + # Mint more of an existing token + contract.mint( + [ + sp.record(to_=alice.address, amount=4, token=sp.variant("existing", 0)), + sp.record(to_=bob.address, amount=4, token=sp.variant("existing", 1)), + ], + _sender=admin, + ) + scenario.verify( + _get_balance(contract, sp.record(owner=alice.address, token_id=0)) == 10 + ) + scenario.verify( + _get_balance(contract, sp.record(owner=bob.address, token_id=0)) == 4 + ) + scenario.verify( + _get_balance(contract, sp.record(owner=alice.address, token_id=1)) == 3 + ) + scenario.verify( + _get_balance(contract, sp.record(owner=bob.address, token_id=1)) == 11 + ) + scenario.verify(_total_supply(contract, sp.record(token_id=0)) == 14) + scenario.verify(_total_supply(contract, sp.record(token_id=1)) == 14) + ``` + +1. Add a test to verify that other users can't mint tokens: + + ```smartpy + # Other users can't mint tokens + contract.mint( + [ + sp.record(to_=alice.address, amount=4, token=sp.variant("existing", 0)), + ], + _sender=alice, + _valid=False + ) + ``` + +1. Add a test to verify that the admin account can create a new token type: + + ```smartpy + # Create a token type + tok2_md = fa2.make_metadata(name="Token Two", decimals=0, symbol="Tok2") + contract.mint( + [ + sp.record(to_=alice.address, amount=5, token=sp.variant("new", tok2_md)), + ], + _sender=admin, + ) + scenario.verify( + _get_balance(contract, sp.record(owner=alice.address, token_id=2)) == 5 + ) + ``` + +1. Add a test to verify that users can burn their tokens but not other accounts' tokens: + + ```smartpy + scenario.h2("Burn tokens") + + # Verify that you can burn your own token + contract.burn([sp.record(token_id=2, from_=alice.address, amount=1)], _sender=alice) + scenario.verify( + _get_balance(contract, sp.record(owner=alice.address, token_id=2)) == 4 + ) + # Verify that you can't burn someone else's token + contract.burn( + [sp.record(token_id=2, from_=alice.address, amount=1)], + _sender=bob, + _valid=False, + ) + scenario.verify( + _get_balance(contract, sp.record(owner=alice.address, token_id=2)) == 4 + ) + scenario.verify( + _total_supply(contract, sp.record(token_id=2)) == 4 + ) + ``` + +1. Run the `python fa2_fungible.py` command to compile and test your contract. +If you see any errors, make sure that your code matches the code above or compare with the completed contract here: [part_2_complete.py](https://github.com/trilitech/tutorial-applications/blob/main/smartpy_fa2_fungible/part_2_complete.py). + +Note that there are many more output files in the `fa2_lib_fungible` folder. +The SmartPy compiler creates output files for each call to an entrypoint in the test scenario. +You can use these files to verify that the scenario is testing the contract properly. + +You can also use these files as precompiled parameters for contract calls, as shown in the next section. + +## (Optional) Test the contract in the Octez client mockup mode + +You can test the mint and burn entrypoints in mockup mode, but you must be sure to deploy the contract with an address that you can use as the administrator, as described in these steps: + +1. Get the address of one of the existing bootstrap accounts in the mockup by running this command: + + ```bash + mockup-client list known addresses + ``` + +1. Replace the first address in the initial storage value in the `step_003_cont_0_storage.tz` file with the bootstrap account address. +For example, the file might look like this, with `tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx` as the bootstrap account: + + ```michelson + (Pair "tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx" (Pair {Elt (Pair "tz1Rp4Bv8iUhYnNoCryHQgNzN2D7i3L1LF9C" 1) 10; Elt (Pair "tz1WxrQuZ4CK1MBUa2GqUWK1yJ4J6EtG1Gwi" 0) 10} (Pair {} (Pair 2 (Pair {} (Pair {Elt 0 10; Elt 1 10} {Elt 0 (Pair 0 {Elt "decimals" 0x30; Elt "name" 0x546f6b656e205a65726f; Elt "symbol" 0x546f6b30}); Elt 1 (Pair 1 {Elt "decimals" 0x30; Elt "name" 0x546f6b656e204f6e65; Elt "symbol" 0x546f6b31})})))))) + ``` + +1. Deploy the contract by running this command: + + ```bash + mockup-client originate contract smartpy_fa2_fungible \ + transferring 0 from bootstrap1 \ + running fa2_lib_fungible/step_003_cont_0_contract.tz \ + --init "$(cat fa2_lib_fungible/step_003_cont_0_storage.tz)" --burn-cap 3 --force + ``` + +1. Mint more of an existing token by following these steps: + + 1. Open the file `fa2_lib_fungible/log.txt`. + + 1. Find the output file that shows the parameters for the call to the `mint` entrypoint. + For example, this logging information shows that the parameters are in the file `fa2_lib_fungible/step_028_cont_0_params.tz`: + + ``` + h2: Mint tokens + file fa2_lib_fungible/step_028_cont_0_params.py + file fa2_lib_fungible/step_028_cont_0_params.tz + file fa2_lib_fungible/step_028_cont_0_params.json + Executing mint([sp.record(to_ = sp.address('tz1WxrQuZ4CK1MBUa2GqUWK1yJ4J6EtG1Gwi'), token = existing(0), amount = 4), sp.record(to_ = sp.address('tz1Rp4Bv8iUhYnNoCryHQgNzN2D7i3L1LF9C'), token = existing(1), amount = 4)])... + ``` + + 1. Use that output file as the parameter for a call to the `mint` entrypoint. + For example, this command uses the file `fa2_lib_fungible/step_028_cont_0_params.tz`: + + ```bash + mockup-client --wait none transfer 0 \ + from bootstrap1 to smartpy_fa2_fungible --entrypoint "mint" \ + --arg "$(cat fa2_lib_fungible/step_028_cont_0_params.tz)" --burn-cap 2 + ``` + + 1. Run this command to use the `get_balance_of` view to see that the tokens have been minted and added to the account: + + ```bash + mockup-client run view get_balance_of on contract smartpy_fa2_fungible with input '{Pair "tz1WxrQuZ4CK1MBUa2GqUWK1yJ4J6EtG1Gwi" 0}' + ``` + + The response shows that the account now has 14 of token type 0: + + ```michelson + { Pair (Pair "tz1WxrQuZ4CK1MBUa2GqUWK1yJ4J6EtG1Gwi" 0) 14 } + ``` + +Now you have an FA2 token contract with minting and burning functionality. +In the next part, you add metadata to provide information about the contract and its tokens to apps such as wallets. diff --git a/sidebars.js b/sidebars.js index 02793cc90..f3d95337a 100644 --- a/sidebars.js +++ b/sidebars.js @@ -308,6 +308,21 @@ const sidebars = { 'tutorials/build-your-first-app/getting-information', ], }, + { + type: 'category', + label: 'Create a fungible token with the SmartPy FA2 library', + link: { + type: 'doc', + id: 'tutorials/smartpy-fa2-fungible', + }, + items: [ + 'tutorials/smartpy-fa2-fungible/basic-fa2-token', + 'tutorials/smartpy-fa2-fungible/minting-and-burning', + 'tutorials/smartpy-fa2-fungible/adding-metadata', + 'tutorials/smartpy-fa2-fungible/customizing-operations', + 'tutorials/smartpy-fa2-fungible/deploying-contract', + ], + }, 'tutorials/create-an-nft/nft-taquito', 'tutorials/create-an-nft/nft-tznft', { diff --git a/static/img/tutorials/fa2-fungible-in-temple.png b/static/img/tutorials/fa2-fungible-in-temple.png new file mode 100644 index 000000000..f0bcd35f2 Binary files /dev/null and b/static/img/tutorials/fa2-fungible-in-temple.png differ diff --git a/static/img/tutorials/fa2-fungible-mint-bcd.png b/static/img/tutorials/fa2-fungible-mint-bcd.png new file mode 100644 index 000000000..f2cb1410b Binary files /dev/null and b/static/img/tutorials/fa2-fungible-mint-bcd.png differ diff --git a/static/img/tutorials/fa2-fungible-temple-add-asset.png b/static/img/tutorials/fa2-fungible-temple-add-asset.png new file mode 100644 index 000000000..26415de27 Binary files /dev/null and b/static/img/tutorials/fa2-fungible-temple-add-asset.png differ diff --git a/static/img/tutorials/fa2-fungible-temple-adding-token.png b/static/img/tutorials/fa2-fungible-temple-adding-token.png new file mode 100644 index 000000000..be203574f Binary files /dev/null and b/static/img/tutorials/fa2-fungible-temple-adding-token.png differ diff --git a/static/img/tutorials/fa2-fungible-temple-assets-list.png b/static/img/tutorials/fa2-fungible-temple-assets-list.png new file mode 100644 index 000000000..3a7d51906 Binary files /dev/null and b/static/img/tutorials/fa2-fungible-temple-assets-list.png differ diff --git a/static/img/tutorials/fa2-fungible-temple-manage-assets.png b/static/img/tutorials/fa2-fungible-temple-manage-assets.png new file mode 100644 index 000000000..305159f26 Binary files /dev/null and b/static/img/tutorials/fa2-fungible-temple-manage-assets.png differ diff --git a/static/img/tutorials/pinata-pinned-file.png b/static/img/tutorials/pinata-pinned-file.png new file mode 100644 index 000000000..f0efd5daa Binary files /dev/null and b/static/img/tutorials/pinata-pinned-file.png differ