# 第一个 NFT

本教程通过以下的分步步骤描述如何在 Aptos 区块链上创建和转移 NFT。核心 NFT 或代币的 Aptos 实例可以在 [token.move](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-token/sources/token.move) 中找到。

## 第 1 步：选择一个 SDK

* [Aptos 官方 Typescript SDK](https://aptos.dev/sdks/typescript-sdk)
* [Aptos 官方 Python SDK](https://aptos.dev/sdks/python-sdk)
* Aptos 官方 Rust SDK —— 待发布

## 第 2 步：运行示例

每个 SDK 都提供一个示例目录。本教程涵盖了该 `simple-nft` 示例。

克隆 `aptos-core`：

```bash
git clone git@github.com:aptos-labs/aptos-core.git ~/aptos-core
```

*   Typescript

    导航至 Typescript SDK 示例目录：

    ```bash
    cd ~/aptos-core/ecosystem/typescript/sdk/examples/typescript
    ```

    安装必要的依赖项：

    ```bash
    yarn install
    ```

    运行 `simple-nft` 示例：

    ```bash
    yarn run simple_nft
    ```
*   Python

    导航至 Python SDK 示例目录：

    ```bash
    cd ~/aptos-core/ecosystem/python/sdk
    ```

    安装必要的依赖项：

    ```bash
    curl -sSL https://install.python-poetry.org | python3
    poetry update
    ```

    运行 `simple-nft` 示例：

    ```bash
    poetry run python -m examples.simple-nft
    ```
*   Rust

    待发布

## 第 3 步：理解输出

执行 simple-nft 示例后，应出现以下输出，但某些值会有所不同：

```rust
=== Addresses ===
Alice: 0x9df0f527f3a0b445e4d5c320cfa269cdefafc7cd1ed17ffce4b3fd485b17aafb
Bob: 0xfcc74af84dde26b0050dce35d6b3d11c60f5c8c58728ca3a0b11035942a0b1de

=== Initial Coin Balances ===
Alice: 20000
Bob: 20000

=== Creating Collection and Token ===
Alice's collection: {
    "description": "Alice's simple collection",
    "maximum": "18446744073709551615",
    "mutability_config": {
        "description": false,
        "maximum": false,
        "uri": false
    },
    "name": "Alice's",
    "supply": "1",
    "uri": "https://aptos.dev"
}
Alice's token balance: 1
Alice's token data: {
    "default_properties": {
        "map": {
            "data": []
        }
    },
    "description": "Alice's simple token",
    "largest_property_version": "0",
    "maximum": "1",
    "mutability_config": {
        "description": false,
        "maximum": false,
        "properties": false,
        "royalty": false,
        "uri": false
    },
    "name": "Alice's first token",
    "royalty": {
        "payee_address": "0x9df0f527f3a0b445e4d5c320cfa269cdefafc7cd1ed17ffce4b3fd485b17aafb",
        "royalty_points_denominator": "1000000",
        "royalty_points_numerator": "0"
    },
    "supply": "1",
    "uri": "https://aptos.dev/img/nyan.jpeg"
}

=== Transferring the token to Bob ===
Alice's token balance: 0
Bob's token balance: 1

=== Transferring the token back to Alice using MultiAgent ===
Alice's token balance: 1
Bob's token balance: 0
```

该示例演示了：

* 初始化 REST 和 Faucet 客户端
* 创建 Alice 和 Bob 两个帐户
* 为 Alice 和 Bob 的帐户提供资金
* 使用 Alice 的帐户创建一个集合和一个代币
* Alice 提供代币，Bob 领取代币
* Bob 通过一个多代理交易单方面地将代币发送给 Alice

## 第 4 步：深入了解 SDK

*   Typescript

    💡 **见完整示例**

    查看 [`[simple_nft]`](https://github.com/aptos-labs/aptos-core/blob/main/ecosystem/typescript/sdk/examples/typescript/simple_nft.ts) 完整代码，按照以下步骤操作
*   Python

    💡 **见完整示例**

    查看 [`[simple_nft]`](https://github.com/aptos-labs/aptos-core/blob/main/ecosystem/python/sdk/examples/simple-nft.py) 完整代码，按照以下步骤操作
*   Rust

    待发布

### 第 4.1 步：初始化客户端

在第 1 步中，示例初始化 API 和 Faucet 客户端。

* API 客户端与 REST API 交互，并且
* Faucet 客户端与 devnet Faucet 服务交互以创建和注资帐户
*   Typescript

    ```tsx
    const client = new AptosClient(NODE_URL);
    const faucetClient = new FaucetClient(NODE_URL, FAUCET_URL);
    ```

    使用 API 客户端，我们可以创建一个 T`okenClient`，用于常见的代币操作，例如创建、转移和领取集合和代币等。

    ```tsx
    const tokenClient = new TokenClient(client);
    ```

    common.ts 将 URL 值初始化为这样：

    ```tsx
    export const NODE_URL = process.env.APTOS_NODE_URL || "https://fullnode.devnet.aptoslabs.com";
    export const FAUCET_URL = process.env.APTOS_FAUCET_URL || "https://faucet.devnet.aptoslabs.com";
    ```
*   Python

    ```python
    rest_client = RestClient(NODE_URL)
    faucet_client = FaucetClient(FAUCET_URL, rest_client)
    ```

    `common.py` 将这些值初始化为这样：

    ```python
    NODE_URL = os.getenv("APTOS_NODE_URL", "https://fullnode.devnet.aptoslabs.com/v1")
    FAUCET_URL = os.getenv(
        "APTOS_FAUCET_URL", "https://faucet.devnet.aptoslabs.com"
    )
    ```

    💡 **提示**

    默认情况下，两个服务等 URL 都指向 Aptos devnet 服务。然而，它们可以通过以下环境变量进行配置。
*   Rust

    待发布

### 第 4.2 步：创建本地账户

下一步是在本地创建两个帐户。帐户代表链上和链下的状态。链下状态包括一个地址和用户验证所有权的公钥、私钥对。这一步演示了如何生成链下状态。

*   Typescript

    ```tsx
    const alice = new AptosAccount();
    const bob = new AptosAccount();
    ```
*   Python

    ```python
    alice = Account.generate()
    bob = Account.generate()
    ```
*   Rust

    待发布

### 第 4.3 步：创建链上账户

在 Aptos 中，每个帐户都必须有一个链上表示，以支持接收代币和货币，以及在其他 dApp 中进行互动。一个帐户代表了一个存储资产的媒介，因此必须明确创建。这个示例利用 Faucet 来创建 Alice 和 Bob 的帐户。只有 Alice 帐户里有资金：

*   Typescript

    ```tsx
    await faucetClient.fundAccount(alice.address(), 20_000);
    await faucetClient.fundAccount(bob.address(), 20_000);
    ```
*   Python

    ```python
    faucet_client.fund_account(alice.address(), 20_000)
    faucet_client.fund_account(bob.address(), 20_000)
    ```
*   Rust

    待发布

### 第 4.4 步：创建一个集合

现在开始创建代币的过程。首先，创建者必须创建一个集合来存储代币。一个集合可以包含零个、一个或多个不同的代币。集合只是一个容器，并不限制代币的属性：

*   Typescript

    应用程序将调用 `creatColletion`：

    ```tsx
    const txnHash1 = await tokenClient.createCollection(
      alice,
      collectionName,
      "Alice's simple collection",
      "https://alice.com",
    );
    ```

    `createCollection`的函数签名。它返回一个交易哈希值：

    ```tsx
    async createCollection(
      account: AptosAccount,
      name: string,
      description: string,
      uri: string,
      maxAmount: BCS.AnyNumber = MAX_U64_BIG_INT,
    ): Promise<string> {
    ```
*   Python

    应用程序将调用 `create_colletion`：

    ```python
    txn_hash = rest_client.create_collection(
        alice, collection_name, "Alice's simple collection", "https://aptos.dev"
    )
    ```

    `create_colletion`的函数签名。它返回一个交易哈希值：

    ```python
    def create_collection(
        self, account: Account, name: str, description: str, uri: str
    ) -> str:
    ```
*   Rust

    待发布

### 第 4.5 步：创建一个代币

要创建一个代币，创建者必须指定一个相关的集合。一个代币必须与一个集合相关联，并且该集合必须有可以被铸造的剩余代币。有许多与代币相关的属性，但辅助 API 只公开了创建静态内容所需的最小数量。

*   Typescript

    应用程序将调用 `creatToken`：

    ```tsx
    const txnHash2 = await tokenClient.createToken(
      alice,
      collectionName,
      tokenName,
      "Alice's simple token",
      1,
      "https://aptos.dev/img/nyan.jpeg",
    );
    ```

    `createToken`的函数签名。它返回一个交易哈希值：

    ```tsx
    async createToken(
      account: AptosAccount,
      collectionName: string,
      name: string,
      description: string,
      supply: number,
      uri: string,
      max: BCS.AnyNumber = MAX_U64_BIG_INT,
      royalty_payee_address: MaybeHexString = account.address(),
      royalty_points_denominator: number = 0,
      royalty_points_numerator: number = 0,
      property_keys: Array<string> = [],
      property_values: Array<string> = [],
      property_types: Array<string> = [],
    ): Promise<string> {
    ```
*   Python

    应用程序将调用 `create_token`：

    ```python
    txn_hash = rest_client.create_token(
        alice,
        collection_name,
        token_name,
        "Alice's simple token",
        1,
        "https://aptos.dev/img/nyan.jpeg",
        0,
    )
    ```

    `create_token`的函数签名。它返回一个交易哈希值：

    ```python
    def create_token(
        self,
        account: Account,
        collection_name: str,
        name: str,
        description: str,
        supply: int,
        uri: str,
        royalty_points_per_million: int,
    ) -> str:
    ```
*   Rust

    待发布

### 第 4.6 步：读取代币和集合的元数据

集合和代币元数据都存储在创建者帐户的 `Collection` 表中。SDK 为查询这些特定的表格提供了便利的封装器：

*   Typescript

    读取集合的元数据：

    ```tsx
    const collectionData = await tokenClient.getCollectionData(alice.address(), collectionName);
    console.log(`Alice's collection: ${JSON.stringify(collectionData, null, 4)}`);
    ```

    读取代币的元数据：

    ```tsx
    const tokenData = await tokenClient.getTokenData(alice.address(), collectionName, tokenName);
    console.log(`Alice's token data: ${JSON.stringify(tokenData, null, 4)}`);
    ```

    以下是 `getTokenData` 如何查询代币元数据的：

    ```tsx
    async getTokenData(
      creator: MaybeHexString,
      collectionName: string,
      tokenName: string,
    ): Promise<TokenTypes.TokenData> {
      const creatorHex = creator instanceof HexString ? creator.hex() : creator;
      const collection: { type: Gen.MoveStructTag; data: any } = await this.aptosClient.getAccountResource(
        creatorHex,
        "0x3::token::Collections",
      );
      const { handle } = collection.data.token_data;
      const tokenDataId = {
        creator: creatorHex,
        collection: collectionName,
        name: tokenName,
      };

      const getTokenTableItemRequest: Gen.TableItemRequest = {
        key_type: "0x3::token::TokenDataId",
        value_type: "0x3::token::TokenData",
        key: tokenDataId,
      };

      // We know the response will be a struct containing TokenData, hence the
      // implicit cast.
      return this.aptosClient.getTableItem(handle, getTokenTableItemRequest);
    }
    ```
*   Python

    读取集合的元数据：

    ```python
    collection_data = rest_client.get_collection(alice.address(), collection_name)
    print(
        f"Alice's collection: {json.dumps(collection_data, indent=4, sort_keys=True)}"
    )
    ```

    读取代币的元数据：

    ```python
    token_data = rest_client.get_token_data(
        alice.address(), collection_name, token_name, property_version
    )
    print(
        f"Alice's token data: {json.dumps(token_data, indent=4, sort_keys=True)}"
    )
    ```

    以下是 `get_token_data`如何查询代币元数据的：

    ```python
    def get_token_data(
        self,
        creator: AccountAddress,
        collection_name: str,
        token_name: str,
        property_version: int,
    ) -> Any:
        token_data_handle = self.account_resource(creator, "0x3::token::Collections")[
            "data"
        ]["token_data"]["handle"]

        token_data_id = {
            "creator": creator.hex(),
            "collection": collection_name,
            "name": token_name,
        }

        return self.get_table_item(
            token_data_handle,
            "0x3::token::TokenDataId",
            "0x3::token::TokenData",
            token_data_id,
        )
    ```
*   Rust

    待发布

### 第 4.7 步：读取代币余额

Aptos 内的每一个代币都是一个独立的资产，用户拥有的资产都存储在他们的 `TokenStore` 内。为读取余额：

*   Typescript

    ```tsx
    const aliceBalance1 = await tokenClient.getToken(
      alice.address(),
      collectionName,
      tokenName,
      `${tokenPropertyVersion}`,
    );
    console.log(`Alice's token balance: ${aliceBalance1["amount"]}`);
    ```
*   Python

    ```python
    balance = rest_client.get_token_balance(
        alice.address(), alice.address(), collection_name, token_name, property_version
    )
    print(f"Alice's token balance: {balance}")
    ```
*   Rust

    待发布

### 第 4.8 步：提供和领取代币

许多用户因收到不需要的代币，而可能造成尴尬甚至是严重后果。Aptos 赋予每个帐户所有者权力，决定是否接受单边转账。默认情况下，是不支持单边转账的。所以 Aptos 提供了一个提供和领取代币的框架。

提供代币：

*   Typescript

    ```tsx
    const txnHash3 = await tokenClient.offerToken(
      alice,
      bob.address(),
      alice.address(),
      collectionName,
      tokenName,
      1,
      tokenPropertyVersion,
    );
    ```
*   Python

    ```python
    txn_hash = rest_client.offer_token(
        alice,
        bob.address(),
        alice.address(),
        collection_name,
        token_name,
        property_version,
        1,
    )
    ```
*   Rust

    待发布

领取代币：

*   Typescript

    ```tsx
    const txnHash4 = await tokenClient.claimToken(
      bob,
      alice.address(),
      alice.address(),
      collectionName,
      tokenName,
      tokenPropertyVersion,
    );
    ```
*   Python

    ```python
    txn_hash = rest_client.claim_token(
        bob,
        alice.address(),
        alice.address(),
        collection_name,
        token_name,
        property_version,
    )
    ```
*   Rust

    待发布

### 第 4.9 步：代币的安全单边转账

为支持代币安全单边转账，发送方可以首先要求接收方确认链下的待定转移。这里以多代理交易请求的形式出现。多代理交易包含多个签名，每个链上帐户都有一个签名。然后，Move 可以利用这一点给所有签名的人以签名者级别的权限。对于代币转移，这样确保了接收方确实希望收到这个代币，而不需要使用上述的代币转移框架。

*   Typescript

    ```tsx
    let txnHash5 = await tokenClient.directTransferToken(
      bob,
      alice,
      alice.address(),
      collectionName,
      tokenName,
      1,
      tokenPropertyVersion,
    );
    ```
*   Python

    ```python
    txn_hash = rest_client.direct_transfer_token(
        bob, alice, alice.address(), collection_name, token_name, 0, 1
    )
    ```
*   Rust

    待发布

### 第 4.10 步：实现代币单边转账

即将上线！
