Aptos+Move->快速上手指南

1. 概述

APTOS公链可视为Meta(原Facebook)Libra(后更名Diem)计划搁浅后的续篇。

今年1月底,Diem被Meta出售,一些核心成员出走,组建团队基于Diem的开源代码进行Aptos公链的开发。

其号称在理想状态下,每秒可处理16万笔交易。

链节点使用Rust语言开发。

链上智能合约的开发语言是Move,是 Diem 项目 专门为区块链开发的一种安全可靠的智能合约编程语言,其语法与Rust语言类似。

2. 常用链接

​ (浏览器搜索功能不好使,可以直接使用URL:https://explorer.devnet.aptos.dev/account/0x3e8bc3b97d4710c3cf1e89890756f7872ead687dbd4d9d4f9118b885bf81c5da )

3. 基础操作

( 基于ts sdk,示例代码:https://github.com/aptos-labs/aptos-core/blob/main/developer-docs-site/static/examples/typescript/first_transaction.ts

1)账户创建与测试币申请

从代码中看APTOS的decimals是8位,但目前在钱包中并没有使用小数,全部按wei在显示和使用,包括钱包转账,gas fee,浏览器展示等。

每个账户地址都关联了一个Authentication Key。这个Authentication Key实际上是钱包私钥的公钥。

但账户可以通过rotate的接口来更新这个Authentication Key,从而达到地址不变,但控制私钥变更的目的。

可以使用浏览器插件钱包创建地址和申请测试币。

也可以使用sdk+js脚本,脚本如下:

export const NODE_URL = "https://fullnode.devnet.aptoslabs.com";
export const FAUCET_URL = "https://faucet.devnet.aptoslabs.com";

/** AptosAccount provides methods around addresses, key-pairs */
import { AptosAccount, TxnBuilderTypes, BCS, MaybeHexString } from "aptos";
/** Wrappers around the Aptos Node and Faucet API */
import { AptosClient, FaucetClient } from "aptos";

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

// 创建钱包账户
const alice = new AptosAccount();
const bob = new AptosAccount();
console.log(`Alice: ${alice.address()} Key Seed: ${Buffer.from(alice.signingKey.secretKey).toString("hex")}`);
console.log(`Bob: ${bob.address()} Key Seed: ${Buffer.from(bob.signingKey.secretKey).toString("hex")}`);

// 申请Faucet测试币,最多1_000_000
await faucetClient.fundAccount(alice.address(), 5_000);
await faucetClient.fundAccount(bob.address(), 0);

测试币申请完成后,可以在浏览器中查看余额。

2)账户余额查询

因为APTOS账户余额以状态数据的形式存储在用户地址中,因此需要对数据进行解析。

export async function accountBalance(accountAddress: MaybeHexString): Promise<number | null> {
  const resource = await client.getAccountResource(accountAddress, "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>");
  if (resource == null) {
    return null;
  }

  return parseInt((resource.data as any)["coin"]["value"]);
}

在浏览器中查看地址信息的时候,也是JSON样式的状态数据:

!\image-20220817182221463\

3)发送转账交易

BCS是一种序列化的格式:Binary Canonical Serialization (BCS)

APTOS默认使用BCS格式发送交易。

APTOS使用使用块高度的同时,还使用区块链状态数据库的VERSION版本号。我理解每一笔导致状态变化的交易就会得到一个新的version,而一个块高度内可以包含多个version。(所以version可以理解为总交易序号?)

使用交易中的执行脚本实现用户账户状态数据的变更。

*注意:代码中的TransactionPayloadScriptFunction已经变更,请参考最新的example代码。

/**
 * Transfers a given coin amount from a given accountFrom to the recipient's account address.
 * Returns the transaction hash of the transaction used to transfer.
 */
async function transfer(accountFrom: AptosAccount, recipient: MaybeHexString, amount: number): Promise<string> {
    // 获取token信息,这里是原生币0x1::aptos_coin::AptosCoin
  const token = new TxnBuilderTypes.TypeTagStruct(TxnBuilderTypes.StructTag.fromString("0x1::aptos_coin::AptosCoin"));

  // 构建转账脚本
  const scriptFunctionPayload = new TxnBuilderTypes.TransactionPayloadScriptFunction(
    TxnBuilderTypes.ScriptFunction.natural(
      "0x1::coin",
      "transfer",
      [token],
      [BCS.bcsToBytes(TxnBuilderTypes.AccountAddress.fromHex(recipient)), BCS.bcsSerializeUint64(amount)],
    ),
  );

  // 获取nonce和chainId
  const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([
    client.getAccount(accountFrom.address()),
    client.getChainId(),
  ]);

  // 生成rawTransaction, 填写gasPrice, maxGas和超时信息
  const rawTxn = new TxnBuilderTypes.RawTransaction(
    TxnBuilderTypes.AccountAddress.fromHex(accountFrom.address()),
    BigInt(sequenceNumber),
    scriptFunctionPayload,
    1000n,
    1n,
    BigInt(Math.floor(Date.now() / 1000) + 10),
    new TxnBuilderTypes.ChainId(chainId),
  );

  // 生成BCS格式的交易
  const bcsTxn = AptosClient.generateBCSTransaction(accountFrom, rawTxn);
  // 广播BCS格式的交易
  const pendingTxn = await client.submitSignedBCSTransaction(bcsTxn);

  return pendingTxn.hash;
}

4)使用DApp网页钱包发送普通交易

普通交易转账实际上是对预置合约的调用。

const transaction = {
  type: "entry_function_payload",
  function: `0x1::coin::transfer`,
  arguments: ['0x0aa630f7e14a5bc07d822f35007a5e316cee9f1b359e4bec3a13a0721ff5f471', '200'],
  type_arguments: [`0x1::aptos_coin::AptosCoin`],
};

console.log('transaction', transaction);
let ret = await window.aptos.signAndSubmitTransaction(transaction);
  • type: "entry_function_payload" 交易类型:带参数的合约调用。

  • function: 0x1::coin::transfer调用接口:0x1是合约地址,coin是合约名,transfer是合约方法。

  • type_arguments: [0x1::aptos_coin::AptosCoin] 模板类型。(不同的模板类型会使操作作用在本地的不同状态数据)

    • arguments: ['0x0aa630f7e14a5bc07d822f35007a5e316cee9f1b359e4bec3a13a0721ff5f471', '200'],合约参数。

  • 目前插件钱包只支持以下6个接口函数:

      CONNECT: 'connect',
      DISCONNECT: 'disconnect',
      GET_ACCOUNT_ADDRESS: 'getAccountAddress',
      IS_CONNECTED: 'is_connected',
      SIGN_AND_SUBMIT_TRANSACTION: 'signAndSubmitTransaction',
      SIGN_TRANSACTION: 'signTransaction',
    

    根据社区用户lyn建议,使用下面这个转账接口,才会自动注册账号,上面的不会自动注册。

    "payload": {
            "function": "0x1::aptos_account::transfer",
            "type_arguments": [],
            "arguments": [
                "0x6f755f59b2dac9ee2b7b03f6332fd6bb0721b4d99cb385685f7e4adffc359b3c",
                "333"
            ],
            "type": "entry_function_payload"
        }
    

    4. 智能合约Move Module

    Aptos上的智能合约称做Move Module,合约发布后有一个合约地址,这个合约地址就是发布人的账户地址。

    编译合约需要用的命令行工具下载地址:https://github.com/aptos-labs/aptos-core/releases/

    (使用代码直接编译此工具会报错,建议直接下载编译好的二进制文件)


    *注意,Aptos接口变化极快,script_function_payload类型已经不可用,变成了entry_function_payload

    当前支持3种交易类型:

    • entry_function_payload:调用module接口的交易

    • script_payload:脚本执行交易

    • module_bundle_payload:module部署

    随时检查API手册,观察接口变化:https://fullnode.devnet.aptoslabs.com/v1/spec#/operations/encode_submission

    其中entry_function类似于一种内置在module中的脚本,在函数定义时使用public entry fun xxxx() { }.

    scritp是链下编写,编译成bytecode后,随交易一起执行的脚本。

    module是链上的合约。


    1)命令行工具初始化

    对命令行工具初始化后,即可使用它来部署合约。首先用插件钱包创建一个地址,导出私钥后,执行如下初始化指令:

    $ aptos init
    # 按照提示,选择网络(可默认),然后输入导出的私钥
    

    然后使用如下指令,即可查看地址的状态数据:

    $ aptos account list
    

    2)插件钱包注入对象

    为方便web3开发,插件钱包启用后,会在浏览器内注入一个window.aptos对象,在DApp中使用这个对象可以与插件钱包交互。

    例如获取地址信息:

    await window.aptos.connect()
    await window.aptos.account()
    

    3)部署示例合约

    下载示例合约代码: the hello_blockchain package.

    使用命令行发布合约代码,填写账户地址在发布指令中:

    $ aptos move publish --package-dir /path/to/hello_blockchain/ --named-addresses HelloBlockchain=<address>
    

    (这里的路径需要写绝对路径,相对路径好像不支持)

    其中的**--named-addresses**表示命名替换,将module中的

    module HelloBlockchain::Message {
    

    替换成

    module 0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481::Message {
    

    在状态数据存储中,只能看到这个地址,看不到之前的Module名字。通过这种方式,将合约发布到账户地址上。

    在浏览器上查看此地址,即可在account modules下方看到此合约的bytecode和开放的ABI接口信息,包括名称、函数、数据结构等。

    4)使用web插件钱包调用合约写数据

    web钱包需要先connect,然后才能使用。

    const address = '0xaa630f7e14a5bc07d822f35007a5e316cee9f1b359e4bec3a13a0721ff5f471';
    const message = 'Hello Wanchain';
    const transaction = {
        type: "entry_function_payload",
        function: `${address}::message::set_message`,
        arguments: [stringToHex(message)],
        type_arguments: [],
    };
    await window.aptos.connect();
    let ret = await window.aptos.signAndSubmitTransaction(transaction);
    console.log('ret', ret);
    

    其中address是部署合约的account地址,同时也是合约地址。

    可以更换不同的地址来调用此合约,可以在浏览器中看到,修改的是每个account自己的状态数据。

    示例代码中的set_message函数需要两个参数,函数声明如下:

    public(script) fun set_message(account: signer, message_bytes: vector<u8>)
    

    第一个参数signer由钱包自动提供。用户只需要提供第二个参数即可。

    5) 读取合约数据

    使用getAccountResources接口读取全部的状态数据信息,从中挑选需要读取的项。

    const client = new AptosClient('https://fullnode.devnet.aptoslabs.com/v1');
    console.log('account', await client.getAccount('0xaa630f7e14a5bc07d822f35007a5e316cee9f1b359e4bec3a13a0721ff5f471'));
    console.log('modules', await client.getAccountModules('0xaa630f7e14a5bc07d822f35007a5e316cee9f1b359e4bec3a13a0721ff5f471'));
    console.log('module', await client.getAccountModule('0xaa630f7e14a5bc07d822f35007a5e316cee9f1b359e4bec3a13a0721ff5f471', 'message'));
    console.log('resources', await client.getAccountResources('0xaa630f7e14a5bc07d822f35007a5e316cee9f1b359e4bec3a13a0721ff5f471'));
    

    6) 创建一个代币(coin)

    1. 部署合约

    在Aptos连的标准框架中,有一个专用的代币合约:0x1::coin,使用它可以创建一个标准代币。

    (此外,标准框架中0x1::managed_coin是带有mint/burn方法的标准代币)

    (在浏览器中查看0x1地址的详细信息,可以看到所有的标准框架合约信息:https://explorer.devnet.aptos.dev/account/0x1)

    自定义代币的合约代码如下:(示例代码: aptos-move/move-examples/moon_coin)

    module MoonCoinType::moon_coin {
        struct MoonCoin {}
    }
    

    将这个数据注册到标准代币合约0x1::coin中,即可得到一个自定义代币。

    首先使用第3)节的指令部署合约:

    $ aptos init
    $ aptos move publish --package-dir XXXX --named-addresses MoonCoinType=0xXXXXX
    

    每个不同的代码目录,都需要重新init一次,init的信息回保存到当前目录下的**.aptos/config.yaml**文件中,用完记得删除以保证私钥安全。

    2. 初始化代币

    接下来使用预置合约0x1::managed_coin对我们部署的合约进行初始化initialize

    managed_coin比coin多了mint、burn接口。

    初始化时,填写代币的name, symbol, decimals, monitor_supply等4个信息。

    我理解的初始化,是使用0x1::managed_coin合约的initialize方法对本地状态数据进行配置和变更。

    使用Web钱包对自定义代币初始化代码如下:

    const transaction = {
      type: "entry_function_payload",
      function: `0x1::managed_coin::initialize`,
      arguments: [stringToHex('Moon Coin'), stringToHex('MOON'), 6, false],
      type_arguments: [`${address}::moon_coin::MoonCoin`],
    };
    
    let ret = await window.aptos.signAndSubmitTransaction(transaction);
    

    初始化完成后,可以在浏览器中看到,自定义的代币decimals, name, symbol信息已经有了:

    {
      "decimals": 6,
      "name": "Moon Coin",
      "supply": {
        "vec": []
      },
      "symbol": "MOON"
    }
    

    在初始化后,调用初始化的地址自动成为代币合约的owner。

    3. 注册接收人

    在Aptos链,一个用户想要接收除APTOS默认币以外的代币时,必须先由接受者主动明确的注册register接收这个代币,然后才能接收。(不能随意空投代币)

    接受者通过调用预置合约的此接口来接收预置代币:

    0x1::coins::register<CoinType>:
    

    注册代码:

    const transaction = {
      type: "entry_function_payload",
      function: `0x1::coins::register`,
      arguments: [],
      type_arguments: [`${address}::moon_coin::MoonCoin`],
    };
    
    let ret = await window.aptos.signAndSubmitTransaction(transaction);
    

    注册完成之后,可以在浏览器上看到,在地址的0x1::coin::CoinStore下面多了一个自定义代币类型的数据段:

    0x1::coin::CoinStore<0xb2e77a8f90524cf30f6f6540530b67a66b7b5fb511ec0d0319668aa9bd3a106d::moon_coin::MoonCoin>
    ...
    

    这个字段与原生币类似,只是尖括号内的类型不同,其它的数据字段和事件完全相同。原生币:

    0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>
    

    4.铸造代币

    铸币代码:

    const transaction = {
      type: "entry_function_payload",
      function: `0x1::managed_coin::mint`,
      arguments: ['0xb2e77a8f90524cf30f6f6540530b67a66b7b5fb511ec0d0319668aa9bd3a106d', '1000000'],
      type_arguments: [`${address}::moon_coin::MoonCoin`],
    };
    
    let ret = await window.aptos.signAndSubmitTransaction(transaction);
    

    5. 代币转账

    自定义转账的方法与原生币基本相同。代码如下:(接收地址需要先注册才能接收,否则交易会上链但链上执行失败)

    const transaction = {
      type: "entry_function_payload",
      function: `0x1::coin::transfer`,
      arguments: ['0x8ded6d8821f7e9a4ea9ecdd953cabf29f263866a0e50a0d66cc0561ef5a99db6', '1000'],
      type_arguments: [`${address}::moon_coin::MoonCoin`],
    };
    
    let ret = await window.aptos.signAndSubmitTransaction(transaction);
    

    5. 其它

    rotate:

    APTOS的地址可以更换auth key,实现在账户地址不变的前提下更换(rotate)签名私钥。可以通过这种方式迁移或释放地址权限。

    地址类型:

    地址类型有两种,一种是普通地址,另一种是资源地址resource account,可以由合约或脚本创建,不与创建者地址相同。

    状态访问接口:

    内置的访问状态数据的函数一共有5个:

    move_to: 创建资源

    move_from: 删除资源

    borrow_global: 获取只读引用

    borrow_global_mut: 获取可写入引用

    exists: 判断资源是否存在

    这四个函数只能访问本module内的结构体数据,但可以通过封装公开的函数接口,提供外部访问本地状态数据的权限。

    !\image-20220830163511359\

    宏定义函数:

    函数后面带!符号的是宏定义函数,例如assert!()

    函数访问权限:

    Module中,函数访问权限有3种,一种是公开public,一种是不带public,默认只有module内部可以访问,最后一种是public(friend) 只有定义成friend的module有权限访问。因此,原生APT coin转账不需要接收人主动注册(register),因为原生APT coin的module有friend权限,可以自动帮接收地址注册(register)。

    合约升级:

    有权限的地址,可以对部署好的module升级。有3种升级策略:强制(arbitrary),兼容(compatible),不可升级(immutable)。通过在Move.toml文件中可以配置,默认是兼容模式。

    [package]
    name = "MyApp"
    version = "0.0.1"
    upgrade_policy = "compatible"
    ...
    

    https://aptos.dev/guides/move-guides/upgrading-move-code/

    部署为兼容模式的module,可以更改为不可升级,反之则不可。

    兼容模式要求所有旧的全局都不可更改,所有旧的公开api接口定义不可变更。

    钱包签名:

    module函数入口参数的第一个&signer类型参数可省略不填,SDK会自动填写当前钱包。

    合约event发送:

    event的创建需要首先使用 account::new_event_handle<T>(sender) 接口来创建event handler,然后使用 event::emit_event<T>()接口来发送event信息。

    在查询时,有2种形式,一种是通过handler查询,一种是通过guid来查询。

    使用handler查询时,需要输入地址,主存储结构体名称,event handler所属字段名称。

    使用guid查询时,使用 creation_num + account的形式,creation_num是event种的数字,以小端模式uint64数字存储。例如create_num是4时:0x0400000000000000166e192a37e1b17dfdb201a2bc8c4cbc25a6e24c5059c05b8f5188fd9ccf4c76

    查询条件的start是event的序号,每个event序号从0开始递增。

    /// Event emitted when some amount of a coin is deposited into an account.
    struct DepositEvent has drop, store {
        amount: u64,
    }
    ...
    let handler = account::new_event_handle<T>(sender)
    ...
    
    event::emit_event<T>(
        &mut handler,
        DepositEvent { amount: coin.value },
    );
    

    地址校验

    正则表达式:"^(0x)[0-9A-Fa-f]{64}$"

    常用cli指令:

    aptos move init 初始化工程目录(Move.toml文件中AptosFramework的rev设置为devnet,并运行clean)

    aptos move clean 清理缓存(更最新新标准库)

    aptos move compile 编译

    aptos move publish 发布module

    aptos move run 发交易调用module的entry函数

    编译命令:

    $ aptos move compile --package-dir . --named-addresses HelloBlockchain=0x{alice_address_here}
    

    这里编译的示例Move代码中的HelloBlockchain:aptos-core/aptos-move/move-examples/hello_blockchain

    这里的--named-addresses后面的地址,是提前生成好的钱包地址。

    使用这个地址来部署这个合约。同时,这个合约地址也是部署者的地址。

    其它用户地址访问这个合约时,修改的是自己地址下的状态数据。

    HelloBlockchain示例代码可以为调用者地址增加一个MessageHolder字段,并存储设置的message信息。

    调用成功后,在浏览器中可以查看调用者地址状态数据时,即可看到新增的数据字段:

    !\image-20220818114723563\

    使用aptos move run 指令执行时,args类型支持:[u8, u64, u128, bool, hex, string, address, raw]。

    查询event时,可以使用序号或guid,使用序号的方式为:

    curl --request GET \
      --url https://fullnode.devnet.aptoslabs.com/v1/accounts/address/events/creation_number \
      --header 'Content-Type: application/json'