你的第一笔交易

本教程介绍了 Aptos SDK 以及如何生成、提交和验证提交给 Aptos 区块链的交易。我们将运行 transfer-coin 的示例。

第 1 步:选择一个 SDK

第 2 步:运行示例代码

克隆 aptos-core 仓库:

git clone git@github.com:aptos-labs/aptos-core.git ~/aptos-core
  • Typescript

    进入到 Typescript SDK 示例的路径下

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

    安装必要的依赖:

    yarn install

    运行 transfer_coin 示例代码

    yarn run transfer_coin
  • Python

    进入到 Python SDK 示例的路径下:

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

    安装必要的依赖:

    curl -sSL <https://install.python-poetry.org> | python3
    poetry update

    运行 transfer-coin 示例代码

    poetry run python -m examples.transfer-coin
  • Rust

    进入到 Rust SDK 示例的路径下:

    cd ~/aptos-core/sdk

    运行 transfer-coin 示例代码

    cargo run --example transfer-coin:

第 3 步:程序输出解析

在你运行上述示例中的命令后,将会出现类似以下的输出:

=== Addresses ===
Alice: 0x0baec07bfc42f8018ea304ddc307a359c1c6ab20fbce598065b6cb19acff7043
Bob: 0xc98ceafadaa32e50d06d181842406dbbf518b6586ab67cfa2b736aaddeb7c74f

=== Initial Balances ===
Alice: 20000
Bob: 0

=== Intermediate Balances ===
Alice: 18996
Bob: 1000

=== Final Balances ===
Alice: 17992
Bob: 2000

上面的输出显示,transfer-coin例子执行了以下步骤。

  • 初始化 REST 和 Faucet 客户端。

  • 创建两个账户:Alice 和 Bob。

    • 通过 Faucet 接口创建 Alice 的账户,并向其注资。

    • 通过 Faucet 接口创建 Bob 的账户。

  • 将 1000 个代币从 Alice 的地址转到 Bob 的地址。

  • 爱丽丝支付了 4 个单位 gas 费用,以实现这一转账操作。

  • 再一次从 Alice 的地址转移 1000 个代币到 Bob 的地址。

  • 爱丽丝又支付了 4 个单位的 gas 费用来完成这次转账。

接下来,请看下面用于完成上述步骤的 SDK 接口的详细解释。

第 4 步:深入理解 SDK 原理

transfer-coin 示例代码使用辅助函数与 REST API 互动。本节将对每个调用进行回顾,并对功能进行深入分析。

  • Typescript

  • Python

  • Rust

第 4.1 步:初始化客户端

第一步, transfer-coin 实例中初始化了 REST 和 faucet 的客户端。

  • REST 客户端与 REST API 进行交互

  • faucet客户端与 devnet Faucet 服务交互,用于创建账户和向账户中注入代币。

  • Typescript

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

    使用API客户端,我们可以创建一个 CoinClient,我们用它来进行常见的代币操作,如转移代币和检查余额。

    const coinClient = new CoinClient(client);

    common.ts 初始化了以下的 URL 值。

    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

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

    [common.py] 初始化了以下配置值

    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>")
  • Rust

    let rest_client = Client::new(NODE_URL.clone());
    let faucet_client = FaucetClient::new(FAUCET_URL.clone(), NODE_URL.clone());

    使用 client API,我们可以创建一个 CoinClient 对象,我们用它来进行常见的硬币操作,如转移硬币和检查余额。

    let coin_client = CoinClient::new(&rest_client);

    在本例中,我们按照如下方法配置测试网 URL

    static NODE_URL: Lazy<Url> = Lazy::new(|| {
        Url::from_str(
            std::env::var("APTOS_NODE_URL")
                .as_ref()
                .map(|s| s.as_str())
                .unwrap_or("<https://fullnode.devnet.aptoslabs.com>"),
        )
        .unwrap()
    });
    
    static FAUCET_URL: Lazy<Url> = Lazy::new(|| {
        Url::from_str(
            std::env::var("APTOS_FAUCET_URL")
                .as_ref()
                .map(|s| s.as_str())
                .unwrap_or("<https://faucet.devnet.aptoslabs.com>"),
        )
        .unwrap()
    });

请注意: 默认情况下,两个服务的 URL 都指向 Aptos devnet 服务。它们也可以通过以下环境变量进行配置。

  • APTOS_NODE_URL

  • APTOS_FAUCET_URL </aside>

第 4.2 步:在本地创建地址

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

  • Typescript

    const alice = new AptosAccount();
    const bob = new AptosAccount();
  • Python

    alice = Account.generate()
    bob = Account.generate()
  • Rust

    let mut alice = LocalAccount::generate(&mut rand::rngs::OsRng);
    let bob = LocalAccount::generate(&mut rand::rngs::OsRng);

第 4.3 步:创建链上账户rust

在 Aptos 网络中,每个账户都必须有一个链上表示,以支持接收代币和硬币,以及在其他 DApps 中进行互动。一个账户代表了一个存储资产的媒介,因此它必须明确地被创建。这个例子利用 Faucet 提供的接口来创建和资助 Alice 的账户;对于 Bob 的账户,我们只做创建的操作:

  • Typescript

    await faucetClient.fundAccount(alice.address(), 20_000);
    await faucetClient.fundAccount(bob.address(), 0);type
  • Python

    faucet_client.fund_account(alice.address(), 20_000)
    faucet_client.fund_account(bob.address(), 0)
  • Rust

    faucet_client
        .fund(alice.address(), 20_000)
        .await
        .context("Failed to fund Alice's account")?;
    faucet_client
        .create_account(bob.address())
        .await
        .context("Failed to fund Bob's account")?;

第 4.4 步:读取账户余额

在这一步中,我们将使用 Aptos SDK 提供的能力请求一个资源,并且读取该资源中的成员变量

  • Typescript

    console.log(`Alice: ${await coinClient.checkBalance(alice)}`);
    console.log(`Bob: ${await coinClient.checkBalance(bob)}`);

    源码逻辑:TypeScript SDK 提供的 CoinClient 下的 checkBalance 函数查询了当前账户地址下所有资源,然后过滤出 Aptos 测试代币(APTOS_COIN)所在的资源,并且读取了当前的值,即用户持有的 Aptos 测试代币的余额。

    async checkBalance(
      account: AptosAccount,
      extraArgs?: {
        // The coin type to use, defaults to 0x1::aptos_coin::AptosCoin
        coinType?: string;
      },
    ): Promise<bigint> {
      const coinType = extraArgs?.coinType ?? APTOS_COIN;
      const typeTag = `0x1::coin::CoinStore<${coinType}>`;
      const resources = await this.aptosClient.getAccountResources(account.address());
      const accountResource = resources.find((r) => r.type === typeTag);
      return BigInt((accountResource!.data as any).coin.value);
    }
  • Python

    print(f"Alice: {rest_client.account_balance(alice.address())}")
    print(f"Bob: {rest_client.account_balance(bob.address())}")

    源码逻辑:Python SDK 的 account_balance 函数直接查询了 Aptos 测试代币所在的资源,并且读取了当前的值,即当前地址下 Aptos 测试代币的余额

    def account_balance(self, account_address: str) -> int:
        """Returns the test coin balance associated with the account"""
        return self.account_resource(
            account_address, "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>"
        )["data"]["coin"]["value"]
  • Rust

    println!(
        "Alice: {:?}",
        coin_client
            .get_account_balance(&alice.address())
            .await
            .context("Failed to get Alice's account balance the second time")?
    );
    println!(
        "Bob: {:?}",
        coin_client
            .get_account_balance(&bob.address())
            .await
            .context("Failed to get Bob's account balance the second time")?
    );

    源码逻辑:rust SDK 的 get_account_resource 函数直接查询了 Aptos 测试代币所在的资源,并且读取了当前的值,即当前地址下 Aptos 测试代币的余额

    let balance = self
        .get_account_resource(address, "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>")
        .await?;

第 4.5 步:转账

和 Step 4.4 一样,这是将 Aptos 代币从 Alice 的地址转移到 Bob 的地址另一个辅助步骤。对于正确生成的交易,API 将返回一个交易哈希值,可以在后续步骤中使用,以检查交易状态。Aptos 区块链在提交时执行了一些验证检查,如果其中任何一项失败,用户将得到一个错误的响应。这些验证包括交易签名,未使用的序列号,以及将交易提交给适当的链。

  • Typescript

    let txnHash = await coinClient.transfer(alice, bob, 1_000);

    源码逻辑:transfer 函数生成了一个交易的 payload 信息,并且让客户端签署,发送,最后等待交易发送的响应结果

    async transfer(
      from: AptosAccount,
      to: AptosAccount,
      amount: number | bigint,
      extraArgs?: {
        // The coin type to use, defaults to 0x1::aptos_coin::AptosCoin
        coinType?: string;
        maxGasAmount?: BCS.Uint64;
        gasUnitPrice?: BCS.Uint64;
        expireTimestamp?: BCS.Uint64;
      },
    ): Promise<string> {
      const coinTypeToTransfer = extraArgs?.coinType ?? APTOS_COIN;
      const payload = this.transactionBuilder.buildTransactionPayload(
        "0x1::coin::transfer",
        [coinTypeToTransfer],
        [to.address(), amount],
      );
      return this.aptosClient.generateSignSubmitTransaction(from, payload, extraArgs);
    }

    aptosClient 中,generateSignSubmitTransaction 函数做了以下的事情:

    const rawTransaction = await this.generateRawTransaction(sender.address(), payload, extraArgs);
    const bcsTxn = AptosClient.generateBCSTransaction(sender, rawTransaction);
    const pendingTransaction = await this.submitSignedBCSTransaction(bcsTxn);
    return pendingTransaction.hash;

    我们一步步来看代码的逻辑:

    1. transfer 在内部是 Coin Move 模块 中的一个EntryFunction,即 Move 中的一个入口函数,可以直接调用。

    2. Move函数被存储在 coin 模块上: 0x1::coin

    3. 因为 coin 模块可以被其他 coin 使用,所以转移时必须明确指定要转移的 coin 类型。如果没有指定coinType,则默认为0x1::aptos_coin::AptosCoin

  • Python

    txn_hash = rest_client.transfer(alice, bob.address(), 1_000)

    源码逻辑:Python SDK 生成交易,签署并发送该交易,最后等待交易发送的响应结果。

    def bcs_transfer(
        self, sender: Account, recipient: AccountAddress, amount: int
    ) -> str:
        transaction_arguments = [
            TransactionArgument(recipient, Serializer.struct),
            TransactionArgument(amount, Serializer.u64),
        ]
    
        payload = EntryFunction.natural(
            "0x1::coin",
            "transfer",
            [TypeTag(StructTag.from_str("0x1::aptos_coin::AptosCoin"))],
            transaction_arguments,
        )
    
        signed_transaction = self.create_single_signer_bcs_transaction(
            sender, TransactionPayload(payload)
        )
        return self.submit_bcs_transaction(signed_transaction)

    我们一步步来看代码的逻辑:

    1. transfer 在内部是 Coin Move 模块 中的一个EntryFunction,即 Move 中的一个入口函数,可以直接调用。

    2. Move函数被存储在 coin 模块上。0x1::coin。

    3. 因为 coin 模块可以被其他 coin 使用,所以转账必须明确地使用 TypeTag 来定义要对哪种 coin 进行转账。

    4. 交易入参必须被放入带有类型指定器(Serializer.{type})的TransactionArguments 中,这将在交易生成时将入参序列化为适当的类型。

  • Rust

    let txn_hash = coin_client
        .transfer(&mut alice, bob.address(), 1_000, None)
        .await
        .context("Failed to submit transaction to transfer coins")?;

    源码逻辑:Rust SDK 生成交易,签署并发送该交易,最后等待交易发送的响应结果。

    let chain_id = self
        .api_client
        .get_index()
        .await
        .context("Failed to get chain ID")?
        .inner()
        .chain_id;
    let transaction_builder = TransactionBuilder::new(
        TransactionPayload::EntryFunction(EntryFunction::new(
            ModuleId::new(AccountAddress::ONE, Identifier::new("coin").unwrap()),
            Identifier::new("transfer").unwrap(),
            vec![TypeTag::from_str(options.coin_type).unwrap()],
            vec![
                bcs::to_bytes(&to_account).unwrap(),
                bcs::to_bytes(&amount).unwrap(),
            ],
        )),
        SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs()
            + options.timeout_secs,
        ChainId::new(chain_id),
    )
    .sender(from_account.address())
    .sequence_number(from_account.sequence_number())
    .max_gas_amount(options.max_gas_amount)
    .gas_unit_price(options.gas_unit_price);
    let signed_txn = from_account.sign_with_transaction_builder(transaction_builder);
    Ok(self
        .api_client
        .submit(&signed_txn)
        .await
        .context("Failed to submit transfer transaction")?
        .into_inner())

    我们一步步来看代码的逻辑:

    1. 首先,我们获取链的 ID,这是建立交易的有效 payload 所必需的。

    2. transfer 在内部是 Coin Move 模块 中的一个EntryFunction,即 Move 中的一个入口函数,可以直接调用。

    3. Move函数被存储在 coin 模块上。0x1::coin。

    4. 因为 coin 模块可以被其他 coin 使用,所以转账必须明确地使用 TypeTag 来定义要对哪种 coin 进行转账。

    5. 交易参数,如 to_account (目标账户地址)和 amount (金额),必须被编码为 BCS,以便与 TransactionBuilder 一起使用。

第 4.6 步:等待交易结果

  • Typescript

    在Typescript中,只要调用 coinClient.transfer 就可以等待交易完成。一旦处理完毕(无论成功与否),该函数将返回 Transaction 对象,如果处理超时,则抛出一个错误。

    如果你想让它在交易没有成功提交时抛出错误,你可以在调用 transfer 时 checkSuccess 设置为 true。

    await client.waitForTransaction(txnHash);
  • Python

    交易的哈希值可以用来查询该交易当前的执行状态,Python SDK 提供了等待当前交易哈希值对应交易输出最终执行结果的能力。

    rest_client.wait_for_transaction(txn_hash)
  • Rust

    交易的哈希值可以用来查询该交易当前的执行状态,Rust SDK 提供了等待当前交易哈希值对应交易输出最终执行结果的能力。

    rest_client
        .wait_for_transaction(&txn_hash)
        .await
        .context("Failed when waiting for the transfer transaction")?;

Last updated