Skip to content

Commit

Permalink
[dev-docs] Improve first token documentation with actual e2e
Browse files Browse the repository at this point in the history
  • Loading branch information
davidiw authored and aptos-bot committed Mar 29, 2022
1 parent 85acfa8 commit 498b750
Show file tree
Hide file tree
Showing 3 changed files with 236 additions and 20 deletions.
2 changes: 1 addition & 1 deletion developer-docs-site/docs/tutorials/first-move-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ This tutorial details how to write, compile, test, publish and interact with Mov
2. Publish the Move Module to the Aptos Blockchain
3. Initialize and interact with resources of the Move Module

This tutorial builds on [Your first transaction](/tutorials/your-first-transaction) and borrows the code as a library for this example. The following tutorial contains example code that can be downloaded in its entirety below:
This tutorial builds on [Your first transaction](/tutorials/your-first-transaction) as a library for this example. The following tutorial contains example code that can be downloaded in its entirety below:

<Tabs>
<TabItem value="python" label="Python" default>
Expand Down
240 changes: 223 additions & 17 deletions developer-docs-site/docs/tutorials/nfts-and-tokens.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,22 @@ sidebar_position: 3
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

Note: The following tutorial is a work in progress.
Note: The following tutorial is a work in progress. Furthermore, the Aptos' (Non-Fungible) Token specification has not been formalized.

# Your first NFT
# Tokens and NFTs in Aptos

An [NFT](https://en.wikipedia.org/wiki/Non-fungible_token) is a non-fungible token or data stored on a blockchain that uniquely defines ownership of an asset. NFTs were first defined in [EIP-721](https://eips.ethereum.org/EIPS/eip-721) and later expanded upon in [EIP-1155](https://eips.ethereum.org/EIPS/eip-1155). NFTs typically comprise of the following aspects:

* A name, the name of the asset
* A name, the name of the asset, which must be unique within a collection
* A description, the description of the asset
* A URL, a non-descript pointer off-chain to more information about the asset could be media such as an image or video or more metadata
* A supply, the total number of units of this NFT, many NFTs have only a single supply while those that have more than one are referred to as editions

Additionally, most NFTs are part of a collection or a set of NFTs with a common attribute, e.g., theme, creator, or minimally contract.
Additionally, most NFTs are part of a collection or a set of NFTs with a common attribute, e.g., theme, creator, or minimally contract. Each collection has a similar set of attributes:

* A name, the name of the collection, which must be unique within the creator's account
* A description, the description of the asset
* A URL, a non-descript pointer off-chain to more information about the asset could be media such as an image or video or more metadata

The Aptos implementation for core NFTs or Tokens can be found in [Token.move](https://github.com/aptos-labs/aptos-core/blob/nft/aptos-move/framework/aptos-framework/sources/Token.move).

Expand All @@ -41,37 +45,34 @@ The Aptos TokenData is defined as:
| ----- | ---- | ----------- |
| `description` | `ASCII::String` | Describes this token |
| `metadata` | `TokenType` | A generic, a optional user defined struct to contain additional information about this token on-chain |
| `name` | `ASCII::String` | The name of this token |
| `name` | `ASCII::String` | The name of this token, must be unique within the collection |
| `supply` | `u64` | Total number of editions of this Token |
| `uri` | `ASCII::String` | URL for additional information / media |

Tokens are defined with the move attributes `drop` and `store`, which means that they can be burned and saved to global storage. Tokens, however, cannot be copied the total number in supply cannot be changed, e.g., the balance of one Token copied to a new instance of that Token. Hence because Tokens lack the `copy` attribute, they are finite. The `id` and `balance` of a token provide its globally unique identity.
Tokens are defined with the move attributes `store`, which means that they can be saved to global storage. Tokens cannot be implicitly dropped and must be burned to ensure that the total balance is equal to supply. Tokens cannot be copied. That is the total balance or supply cannot be changed by anyone but the creator due to the lack of a copy operator. Note, the current APIs do not expose the ability for post creation mints. A token can be uniquely identified by either its `id` or by the tuple of `TokenType, collection name, and token name`.

TokenData has the `copy` attribute because none of the fields on that define part of its globally unique identity. Users trading Tokens are advised that two Tokens can share the same TokenData as the Aptos standard does not attempt to identify if a token copies attributes from another. It is worth repeating that it is the `id` of a Token and its `balance` that define a token and the rest of the fields are there to assist in data retrieval and understanding of the token.

TokenType provides for optional data extensibility of the Aptos Token. This data, too, is expected to be copy.
TokenData has the `copy` attribute to support simplicity of token balance splitting. A token split can happen whenever an individual with balance greater than `1` offers another individual a portion of their balance less than their total balance. Users trading Tokens are advised that two Tokens can share the same TokenData as the Aptos standard does not attempt to identify if a token copies attributes from another. Repeating what was said earlier, the token can be uniquely identified by either its `id` or `TokenType`, collection name, and token name. A creator could change any of the values in the set of `TokenType`, collection name, or token name to create similar but not identtcal tokens.

### The Token Collection

Aptos defines a set of collections grouped together by their unique `id`:

```rust
struct Collections<TokenType: copy + drop + store> has key {
collections: Table<ID, Collection<TokenType>>,
collections: Table<ASCII::String, Collection<TokenType>>,
}
```

As the `Collections` has the attribute `key`, it is stored directly to the creators account. It is important to note, that if there were no notion of `Collections` and instead `Collection` had the `key` attribute, an Aptos account could only have a single collection of `TokenType`, which often is not the case.
As the `Collections` has the attribute `key`, it is stored directly to the creators account. It is important to note, that if there were no notion of `Collections` and instead `Collection` had the `key` attribute, an Aptos account could only have a single collection of `TokenType`, which often is not the case. A collection can be looked up in the set of Collections by name, hence enforcing a unique `TokenType` and collect name.

Each collection has the following fields:

| Field | Type | Description |
| ----- | ---- | ----------- |
| `tokens` | `Table<GUID::ID, TokenMetadata<TokenType>>` | Keeps track of all Tokens associated with this collection |
| `claimed_tokens` | `Table<GUID::ID, address>` | Keeps track of where tokens wth `supply == 1` are stored |
| `id` | `GUID::ID` | Unique identifier for this collection |
| `tokens` | `Table<ASCII::String, TokenMetadata<TokenType>>` | Keeps track of all Tokens associated with this collection |
| `claimed_tokens` | `Table<ASCII::String, address>` | Keeps track of where tokens wth `supply == 1` are stored |
| `description` | `ASCII::String` | Describes this collection |
| `name` | `ASCII::String` | The name of this collection |
| `name` | `ASCII::String` | The name of this collection, must be unique within the creators account for the specified `TokenType`. |
| `uri` | `ASCII::String` | URL for additional information / media |
| `count` | `u64` | Total number of distinct Tokens tracked by this collection |
| `maximum` | `Option<u64>` | Optional, maximum amount of tokens that can be minted within this collection |
Expand All @@ -95,6 +96,211 @@ struct Gallery<TokenType: copy + drop + store> has key {

Like the `Collections`, this is stored as a resource on an Aptos account.

## Create Tokens
## Introducing Simple Tokens

As part of our core framework, Aptos provides [SimpleToken](https://github.com/aptos-labs/aptos-core/blob/nft/aptos-move/framework/aptos-framework/sources/SimpleToken.move), which are Tokens that have no metadata, or explicitly a `TokenType` of an empty struct. The motivations for doing so include:

* Creating a new token requires writing Move code
* The Script function for creating a new token must be specialized as Move does not support template types or structs as input arguments
* Template types on script functions add extra friction to writing script functions
* We needed to demonstrate a minimal token for this tutorial

This tutorial will walk you through the process of
* creating your own SimpleToken collection,
* a SimpleToken of our favorite cat,
* and giving that token to someone else.

This tutorial builds on [Your first transaction](/tutorials/your-first-transaction) as a library for this example. The following tutorial contains example code that can be downloaded in its entirety below:

<Tabs>
<TabItem value="python" label="Python" default>

For this tutorial, will be focusing on `first_nft.py` and re-using the `first_transaction.py` library from the previous tutorial.

You can find the python project [here](https://github.com/aptos-labs/aptos-core/tree/main/developer-docs-site/static/examples/python)

</TabItem>
<TabItem value="rust" label="Rust" default>

TODO

</TabItem>
<TabItem value="typescript" label="Typescript" default>

TODO

</TabItem>
</Tabs>
### Creating a Collection

THe Aptos Token enables creators to create finite or unlimited collections. Many NFTs are of a limited nature, where the creator only intends on creating a certain amount forever, this enforces scarcity. Whereas other tokens may have an unlimited nature, for example, a collection used for utility may see new tokens appear over time. SimpleToken collections can be instantiated with either behavior by using the appropriate script function:

Finite, that is no more than a `maximum` number of tokens can ever be minted:
```rust
public(script) fun create_finite_simple_collection(
account: signer,
description: vector<u8>,
name: vector<u8>,
uri: vector<u8>,
maximum: u64,
)
```

Unlimited, that is there is no limit to the number of tokens that can be added to the collection:
```rust
public(script) fun create_unlimited_simple_collection(
account: signer,
description: vector<u8>,
name: vector<u8>,
uri: vector<u8>,
)
```

These script functions can be called via the REST API. The following demonstrates how to call the as demonstrated in the following:

<Tabs>
<TabItem value="python" label="Python" default>

```python
:!: static/examples/python/first_nft.py section_1
```
</TabItem>
<TabItem value="rust" label="Rust" default>

TODO
</TabItem>
<TabItem value="typescript" label="Typescript" default>

TODO
</TabItem>
</Tabs>

### Creating a Token

Tokens can be created after collection creation. To do so, the token must specify the same `collection_name` as specified as the name of a previously created collection `name`. The Move script function to create a `SimpleToken` is:

```rust
public(script) fun create_simple_token(
account: signer,
collection_name: vector<u8>,
description: vector<u8>,
name: vector<u8>,
supply: u64,
uri: vector<u8>,
)
```

These script functions can be called via the REST API. The following demonstrates how to call the as demonstrated in the following:

<Tabs>
<TabItem value="python" label="Python" default>

```python
:!: static/examples/python/first_nft.py section_2
```
</TabItem>
<TabItem value="rust" label="Rust" default>

TODO
</TabItem>
<TabItem value="typescript" label="Typescript" default>

TODO
</TabItem>
</Tabs>

### Giving Away a Token

In Aptos and Move, each token occupies space and has ownership. Because of this, token transfers are not unilateral and require two phase process similar to a bulletin board. The sender must first register that a token is available for the recipient to claim, the recipient must then claim this token. This has been implemented in a proof of concept move module called [`TokenTransfer`](https://github.com/aptos-labs/aptos-core/blob/nft/aptos-move/framework/aptos-framework/sources/TokenTransfers.move). `SimpleToken` provides a few wrapper functions to support transferring to another account, claiming that transfer, or stopping that transfer.

#### Obtaining the Token ID

In order to transfer the token, the sender must first identify the token id based upon knowing the creator's account, the collection name, and the token name. This can be obtained by querying REST:

<Tabs>
<TabItem value="python" label="Python" default>

```python
:!: static/examples/python/first_nft.py section_3
```
</TabItem>
<TabItem value="rust" label="Rust" default>

TODO
</TabItem>
<TabItem value="typescript" label="Typescript" default>

TODO
</TabItem>
</Tabs>

#### Offering the Token

The following Move script function in `SimpleToken` supports transferring a token to another account, effectively registering that the other account can claim the token:

```rust
public(script) fun transfer_simple_token_to(
sender: signer,
receiver: address,
creator: address,
token_creation_num: u64,
amount: u64,
)
```

<Tabs>
<TabItem value="python" label="Python" default>

```python
:!: static/examples/python/first_nft.py section_4
```
</TabItem>
<TabItem value="rust" label="Rust" default>

TODO
</TabItem>
<TabItem value="typescript" label="Typescript" default>

TODO
</TabItem>
</Tabs>

#### Claiming the Token

The following Move script function in `SimpleToken` supports receiving a token provided by the previous function, effectively claiming a token:

```rust
public(script) fun receive_token_from(
sender: signer,
receiver: address,
creator: address,
token_creation_num: u64,
amount: u64,
)
```

<Tabs>
<TabItem value="python" label="Python" default>

```python
:!: static/examples/python/first_nft.py section_5
```
</TabItem>
<TabItem value="rust" label="Rust" default>
TODO
</TabItem>
<TabItem value="typescript" label="Typescript" default>
TODO
</TabItem>
</Tabs>

## Todos for Tokens

## Sharing Tokens
* Add ability for additional mints
* Ensure that at least a single token is produced at time of mint
* Remove drop from token and add explicit burn to make sure supply == total balance
* Move away from token data and toward a notion of an ownership token -- that is the data remains with the collection
* Add an ability for the owner to register that their claim in the claim_tokens
* Add events -- needs feedback on what events
* Provide mutable APIs for tokens
* Write a smoketest for generics and simple token directly
14 changes: 12 additions & 2 deletions developer-docs-site/static/examples/python/first_nft.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def submit_transaction_helper(self, account: Account, payload: Dict[str, Any]):
res = self.submit_transaction(signed_txn)
self.wait_for_transaction(res["hash"])

#:!:>section_1
def create_collection(self, account: Account, description: str, name: str, uri: str):
"""Creates a new collection within the specified account"""

Expand All @@ -29,7 +30,9 @@ def create_collection(self, account: Account, description: str, name: str, uri:
]
}
self.submit_transaction_helper(account, payload)
#<:!:section_1

#:!:>section_2
def create_token(
self,
account: Account,
Expand All @@ -38,7 +41,7 @@ def create_token(
name: str,
supply: int,
uri: str,
):
)
payload = {
"type": "script_function_payload",
"function": f"0x1::SimpleToken::create_simple_token",
Expand All @@ -52,7 +55,9 @@ def create_token(
]
}
self.submit_transaction_helper(account, payload)
#<:!:section_2

#:!:>section_4
def transfer_token_to(
self,
account: Account,
Expand All @@ -73,7 +78,9 @@ def transfer_token_to(
]
}
self.submit_transaction_helper(account, payload)
#<:!:section_4

#:!:>section_5
def receive_token_from(
self,
account: Account,
Expand All @@ -92,14 +99,15 @@ def receive_token_from(
]
}
self.submit_transaction_helper(account, payload)
#<:!:section_5

def stop_token_transfer_to(
self,
account: Account,
receiver: str,
creator: str,
token_creation_num: int,
):
)
payload = {
"type": "script_function_payload",
"function": f"0x1::SimpleToken::stop_simple_token_transfer_to",
Expand All @@ -112,6 +120,7 @@ def stop_token_transfer_to(
}
self.submit_transaction_helper(account, payload)

#:!:>section_3
def get_token_id(self, creator: str, collection_name: str, token_name: str) -> int:
""" Retrieve the token's creation_num, which is useful for non-creator operations """

Expand All @@ -129,6 +138,7 @@ def get_token_id(self, creator: str, collection_name: str, token_name: str) -> i
return int(token["value"]["id"]["creation_num"])

assert False
#<:!:section_3


if __name__ == "__main__":
Expand Down

0 comments on commit 498b750

Please sign in to comment.