# 第一个 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 步：实现代币单边转账

即将上线！
