DAOrayaki |Aptos & Move 实操讲解

本文主要讲解操作 aptos cli 和 aptos sdk

DAOrayaki |Aptos & Move 实操讲解

本文主要讲解操作 aptos cli 和 aptos sdk

DAOrayaki DAO研究奖金池:

资助地址:  DAOrayaki.eth

投票进展:DAO Reviewer  3/0 通 过

研究种类:Aptos,Layer1

创作者:FF@DoraFactory

本文主要讲解操作 aptos cliaptos sdk

Clone Aptos-core Repo

# Clone the Aptos repo.git clone # cd into aptos-core directory.cd aptos-core# Run the scripts/dev_setup.sh Bash script as shown below. This will prepare your developer environment../scripts/dev_setup.sh# Update your current shell environment.source ~/.cargo/env# Skip this step if you are not installing an Aptos node.git checkout --track origin/devnet

启动 Aptos 本地链

Using CLI to Run a Local Testnet | Aptos Docs[1]

  1. 启动本地链
ps: 通过这个方法启动的本地链、数据都会保存在启动这条命令的当前文件夹下,以.aptos/ 文件存在
aptos node run-local-testnet --with-faucet

启动成功:

Completed generating configuration:        Log file: "/Users/greg/.aptos/testnet/validator.log"        Test dir: "/Users/greg/.aptos/testnet"        Aptos root key path: "/Users/greg/.aptos/testnet/mint.key"        Waypoint: 0:74c9d14285ec19e6bd15fbe851007ea8b66efbd772f613c191aa78721cadac25        ChainId: TESTING        REST API endpoint: 0.0.0.0:8080        FullNode network: /ip4/0.0.0.0/tcp/6181Aptos is running, press ctrl-c to exitFaucet is running.  Faucet endpoint: 0.0.0.0:8081

启动成功后会提示 rest api 和 faucet api 的地址。后面需要把这两个信息配置在 aptos cli 环境内。

  1. 配置 aptos cli 环境

为了通过命令行访问和调用本地测试链,我们需要给 aptos cli 根据上面的部署信息配置 config。

PROFILE=localaptos init --profile $PROFILE --rest-url  --faucet-url 

执行过程中,我们会得到如下的输出。我们可以选择输入一个秘钥,也可以默认随机生成

Configuring for profile localUsing command line argument for rest URL Using command line argument for faucet URL Enter your private key as a hex literal (0x...) [Current: None | No input: Generate new key (or keep one if present)]

确认之后,会创建一个账户并使用默认数量的 token 为其注资。

No key given, generating key...Account 7100C5295ED4F9F39DCC28D309654E291845984518307D3E2FE00AEA5F8CACC1 doesn't exist, creating it and funding it with 10000 coinsAptos is now set up for account 7100C5295ED4F9F39DCC28D309654E291845984518307D3E2FE00AEA5F8CACC1!  Run `aptos help` for more information about commands{  "Result": "Success"}

从现在开始,我们就可以通过添加--profile local命令以在本地测试网上运行它们。

--profilekube-config

profile 的配置,会设置执行者地址、node-rest-api、faucet-api 信息。

# 列出cli控制的所有账户aptos account list# 为账户注资:aptos account fund --profile $PROFILE --account $PROFILE# 创建新的资源账户aptos account create-resource-account --profile $PROFILE --seed 1# 编译move合约aptos move compile --package-dir hello_blockchain# 部署合约aptos move publish --package-dir hello_blockchain --named-addresses basecoin= --profile local# 调用合约aptos move run --function-id :::: --profile local# 列出指定账户的modules/resources信息aptos account list --query modules --account 0xa1285adb4b8abedf5faf7a46d260c5844f1f64d59dd9b8869db1543cf5bbadf4 --profile localaptos account list --query resources --account 0x4200c2b801870f20a709abba80b6edb90a45ecd9b8acce9842b93d597602edcf --profile local# 合约升级aptos move publish --upgrade-policy    `arbitrary`, `compatible`, `immutable` 对应 0,1,2    0 不做任何检查,强制替换code,    1 做兼容性检查(同样的public 函数,不能改变已有Resource的内存布局)    2 禁止升级    每次publish的时候会比较链上的policy和此次publish的policy(默认是1),    只有此次的policy小于链上的policy时才允许合约升级

部署一个简单的 Move 合约

module MyCounterAddr::MyCounter {    use std::signer;    struct Counter has key, store {        value:u64,    }    public fun init(account: &signer){        move_to(account, Counter{value:0});    }    public fun incr(account: &signer) acquires Counter {        let counter = borrow_global_mut(signer::address_of(account));        counter.value = counter.value + 1;    }    public entry fun init_counter(account: signer){        Self::init(&account)    }    public entry fun incr_counter(account: signer)  acquires Counter {        Self::incr(&account)    }}

MyCounter 源码分析

module 是发布在特定地址下的打包在一起的一组函数和结构体。使用 script 时需要与已发布的 module 或标准库一起运行,而标准库本身就是在 0x1 地址下发布的一组 module。

module MyCounterAddr::MyCounter{ } 则在该 MyCounterAddr 地址下(对应 Move.toml 下的 MyCounterAddr = "0x4200c2b801870f20a709abba80b6edb90a45ecd9b8acce9842b93d597602edcf")创建一个 module。

use std::signer,是使用标准库下的 signer module,Signer 是一种原生的类似 Resource 的不可复制的类型,它包含了交易发送者的地址。引入 signer 类型的原因之一是要明确显示哪些函数需要发送者权限,哪些不需要。因此,函数不能欺骗用户未经授权访问其 Resource。具体可参考源码[2]

module std::signer {    // Borrows the address of the signer    // Conceptually, you can think of the `signer` as being a struct wrapper arround an    // address    // ```    // struct signer has drop { addr: address }    // ```    // `borrow_address` borrows this inner field    native public fun borrow_address(s: &signer): &address;    // Copies the address of the signer    public fun address_of(s: &signer): address {        *borrow_address(s)    }    /// Return true only if `s` is a transaction signer. This is a spec function only available in spec.    spec native fun is_txn_signer(s: signer): bool;    /// Return true only if `a` is a transaction signer address. This is a spec function only available in spec.    spec native fun is_txn_signer_addr(a: address): bool;}

Struct & Abilities

struct Counter has key, store {    value:u64,}

使用 struct 定义了一个叫做 Counter 的结构体,同时被 key,store 两种限制符修饰。

Move 的类型系统灵活,每种类型都可以定义四种能力(abilities)。

它们定义了类型的值是否可以被复制、丢弃和存储。

这四种 abilities 限制符分别是: Copy, Drop, Store 和 Key。

它们的功能分别是:

  • Copy - 值可以被复制
  • Drop - 在作用域(Scope)结束时值可以被丢弃
  • Key - 值可以作为键值(Key)被「全局存储操作( global storage operations)」进行访问
  • Store - 值可以被 存储 到全局状态。

这里用 key、store 修饰,则表示它不能被复制,也不能被丢弃或重新使用,但是它却可以被安全地存储和转移。

Abilities 的语法

基本类型和内建类型的 abilities 是预先定义好的并且不可改变: integers, vector, addresses 和 boolean 类型的值先天具有 copy、drop 和 store ability。

然而,结构体的 ability 可以按照下面的语法进行添加:

struct NAME has ABILITY [, ABILITY] { [FIELDS] }

一个简单的图书馆例子:

module Library {    // each ability has matching keyword    // multiple abilities are listed with comma    struct Book has store, copy, drop {        year: u64    }    // single ability is also possible    struct Storage has key {        books: vector    }    // this one has no abilities    struct Empty {}}

什么是 Resource

Move 白皮书中详细描述了 Resource 这个概念。最初,它是作为一种名为 resource 的结构体类型被实现,自从引入 ability 以后,它被实现成拥有 Key Store两种 ability 的结构体。Resource 可以安全的表示数字资产,它不能被复制,也不能被丢弃或重新使用,但是它却可以被安全地存储和转移。

Resource 的定义

Resource 是一种用 keystore ability 限制了的结构体:

module M {    struct T has key, store {        field: u8    }}

Resource 的限制

在代码中,Resource 类型有几个主要限制:

  1. Resource 存储在帐户下。因此,只有在分配帐户后才会存在,并且只能通过该帐户访问
  2. 一个帐户同一时刻只能容纳一个某类型的 Resource。
  3. Resource 不能被复制;与它对应的是一种特殊的kindresource,它与copyable不同,这一点在泛型章节中已经介绍。(这里可以抽象到 Rust 的所有权那)
  4. Resource 必需被使用,这意味着必须将新创建的 Resource move到某个帐户下,从帐户移出的 Resource 必须被解构或存储在另一个帐户下。

刚才的案例

struct Counter has key, store {    value:u64,}

所以这里就有一个和 solidity 的区别了,在 eth 上如果需要发行一个新资产,比如 usdc。那这个资产是记录在合约里的某个 map 中。而 move 就不同了,资产是作为 resource 存在用户地址下的。

定义函数

public fun init(account: &signer){    move_to(account, Counter{value:0});}public fun incr(account: &signer) acquires Counter {    let counter = borrow_global_mut(signer::address_of(account));    counter.value = counter.value + 1;}public entry fun init_counter(account: signer){    Self::init(&account)}public entry fun incr_counter(account: signer)  acquires Counter {    Self::incr(&account)}

定义格式则是:

public fun 函数名(参数:参数类型){ }

move 函数默认是私有函数,只能在定义它们的模块中访问。关键字 public 将更改函数的默认可见性并使其公开,即可以从外部访问。

init 方法参数是一个&signer,意味着该方法必须是一个账户合法签名过后才可以调用,move_to则是 move 的一个原语,作用是发布、添加 Counter 资源到 signer 的地址下。Move 的账户模型,code 和 data 是存储在一个账户地址下的。

下面是列举的常用原语

  • move_to< T >(&signer, T):发布、添加类型为 T 的 Resource 到 signer 的地址下。
  • move_from< T >(addr: address): T - 从地址下删除类型为 T 的 Resource 并返回这个资源。
  • borrow_global< T >(addr: address): &T - 返回地址下类型为 T 的 Resource 的不可变引用。
  • borrow_global_mut< T >(addr: address): &mut T - 返回地址下类型为 T 的 Resource 的可变引用。
  • exists< T >(address): bool:判断地址下是否有类型为 T 的 Resource

incr 方法参数也是一个&signer,意味着该方法必须是一个账户合法签名过后才可以调用,

关键字 acquires,放在函数返回值之后,用来显式定义此函数获取的所有 Resource。

Signer::address_of(account) 从签名者中拿到 address

borrow_global_mut 上面有介绍到,可变借用到 address 下的 resource Counter,然后将 Counter 结构体下的 value 进行+1 操作。

这下面的两个方法则是 script 方法,它与上面两个函数有什么区别呢?

  • public fun : 方法可以在任何模块中被调用。
  • public(script) fun / public entry fun:script function 是模块中的入口方法,表示该方法可以通过控制台发起一个交易来调用,就像本地执行脚本一样

下个版本的 Move 会用 public entry fun 替代 public(script) fun

Self 则是代表自身 module。

使用 Aptos Cli 编译、部署、调用合约

# 创建新的测试环境aptos init --profile devtest --rest-url  --faucet-url # 编译move合约aptos move compile --package-dir my-counter# 部署合约# 例如:aptos move publish --package-dir my-counter --named-addresses basecoin=0x8e00bd9827faf171996ef37f006dd622bb5c3e43ec52298a8f37fd38cd59664 --profile devtestaptos move publish --package-dir my-counter --named-addresses basecoin= --profile devtest# 调用合约# 例如:# aptos move run --function-id 0x8e00bd9827faf171996ef37f006dd622bb5c3e43ec52298a8f37fd38cd59664::MyCounter::init_counter --profile devtest# aptos move run --function-id 0x8e00bd9827faf171996ef37f006dd622bb5c3e43ec52298a8f37fd38cd59664::MyCounter::incr_counter --profile devtestaptos move run --function-id :::: --profile devtest# 列出指定账户的modules/resources信息aptos account list --query modules --account 0xa1285adb4b8abedf5faf7a46d260c5844f1f64d59dd9b8869db1543cf5bbadf4 --profile devtestaptos account list --query resources --account 0x4200c2b801870f20a709abba80b6edb90a45ecd9b8acce9842b93d597602edcf --profile devtest

Aptos SDK 调用 Move 合约

编译好合约之后,我们可以通过 sdk 调用我们的合约。

我们可以选择通过 sdk 部署合约,也可以通过 sdk 调用 move 合约。

1、通过 sdk 部署合约

当我们编译完成之后,会在 move 合约文件夹下生成 build/ 文件夹

我们需要把 my-counter/build/Examples/bytecode_modules/MyCounter.mv 文件 copy 到SDK脚本下。

aptos move compile --package-dir my-countercp MyCounter.mv my-counter-sdk-demo/

2、部署合约相关的 sdk 代码

/** Publish a new module to the blockchain within the specified account */export async function publishModule(accountFrom: AptosAccount, moduleHex: string): Promise<string> {  const moudleBundlePayload = new TxnBuilderTypes.TransactionPayloadModuleBundle(    new TxnBuilderTypes.ModuleBundle([new TxnBuilderTypes.Module(new HexString(moduleHex).toUint8Array())]),  );  const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([    client.getAccount(accountFrom.address()),    client.getChainId(),  ]);  const rawTxn = new TxnBuilderTypes.RawTransaction(    TxnBuilderTypes.AccountAddress.fromHex(accountFrom.address()),    BigInt(sequenceNumber),    moudleBundlePayload,    1000n,    1n,    BigInt(Math.floor(Date.now() / 1000) + 10),    new TxnBuilderTypes.ChainId(chainId),  );  const bcsTxn = AptosClient.generateBCSTransaction(accountFrom, rawTxn);  const transactionRes = await client.submitSignedBCSTransaction(bcsTxn);  return transactionRes.hash;}

3、通过 SDK 发送交易

这里,我们以 my-counter 合约中的init_counterincr_counter 为例。

构造两个方法用于调用这两个方法,从而实现客户端调用 initincr 的功能。

async function initCounter(contractAddress: string, accountFrom: AptosAccount): Promise<string> {  const scriptFunctionPayload = new TxnBuilderTypes.TransactionPayloadScriptFunction(    TxnBuilderTypes.ScriptFunction.natural(      `${contractAddress}::MyCounter`, // 合约地址::合约名称      "init_counter", // script 函数方法      [],      [],    ),  );  const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([    client.getAccount(accountFrom.address()),    client.getChainId(),  ]);  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),  );  const bcsTxn = AptosClient.generateBCSTransaction(accountFrom, rawTxn);  const transactionRes = await client.submitSignedBCSTransaction(bcsTxn);  return transactionRes.hash;}async function incrCounter(contractAddress: string, accountFrom: AptosAccount): Promise<string> {  const scriptFunctionPayload = new TxnBuilderTypes.TransactionPayloadScriptFunction(    TxnBuilderTypes.ScriptFunction.natural(      `${contractAddress}::MyCounter`,      "incr_counter",      [],      [],    ),  );  const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([    client.getAccount(accountFrom.address()),    client.getChainId(),  ]);  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),  );  const bcsTxn = AptosClient.generateBCSTransaction(accountFrom, rawTxn);  const transactionRes = await client.submitSignedBCSTransaction(bcsTxn);  return transactionRes.hash;}

4、通过 SDK 获取账户里的资源信息。

resource 是存放在所属的账户地址下的,我们可以根据 account 地址,查询相关的 resource 信息。

getCounter()方法其实就是获取 my-counter 下的 **Counter **资源。

async function getCounter(contractAddress: string, accountAddress: MaybeHexString): Promise<string> {  try {    const resource = await client.getAccountResource(      accountAddress.toString(),      `${contractAddress}::MyCounter::Counter`,    );    return (resource as any).data["value"];  } catch (_) {    return "";  }}

其实这个效果就类似 sdk 里的

aptos account list --query resources --account 0x4200c2b801870f20a709abba80b6edb90a45ecd9b8acce9842b93d597602edcf

最终的主函数

async function main() {  assert(process.argv.length == 3, "Expecting an argument that points to the helloblockchain module");  const contractAddress = "0x173d51b1d50614b03d0c18ffcd958309042a9c0579b6b21fc9efeb48cdf6e0b0"; // 指定之前部署的合约地址  const bob = new AptosAccount(); // 创建一个测试地址 Bob  console.log("\n=== Addresses ===");  console.log(`Bob: ${bob.address()}`);  await faucetClient.fundAccount(bob.address(), 5_000); // 给 Bob 地址空投5000个测试token  console.log("\n=== Initial Balances ===");  console.log(`Bob: ${await accountBalance(bob.address())}`);  await new Promise<void>((resolve) => {    readline.question(      "Update the module with Alice's address, build, copy to the provided path, and press enter.",      () => {        resolve();        readline.close();      },    );  });  const modulePath = process.argv[2];  const moduleHex = fs.readFileSync(modulePath).toString("hex");  console.log('Init Counter Moudle.');  let txHash = await initCounter(contractAddress, bob); // 在bob下init Counter资源,此时bob下的Counter的value为0.  await client.waitForTransaction(txHash);  console.log("\n=== Testing Bob Get Counter Value ===");  console.log(`Initial value: ${await getCounter(contractAddress, bob.address())}`);  console.log('========== Incr Counter Value, 1th ==========');  txHash = await incrCounter(contractAddress, bob); // bob调用一次incrCounter方法,此时Counter为1.  console.log(txHash);  await client.waitForTransaction(txHash);  await Sleep(100);  console.log(`New value: ${await getCounter(contractAddress, bob.address())}`); // 获取bob地址下的Counter值,并输出。  console.log('========== Incr Counter Value, 2th ==========');  txHash = await incrCounter(contractAddress, bob); // bob调用一次incrCounter方法,此时Counter为2.  console.log(txHash);  await client.waitForTransaction(txHash);  await Sleep(100);  console.log(`New value: ${await getCounter(contractAddress, bob.address())}`); // 获取bob地址下的Counter值,并输出。  console.log('========== Incr Counter Value, 3th ==========');  txHash = await incrCounter(contractAddress, bob); // bob调用一次incrCounter方法,此时Counter为3.  console.log(txHash);  await client.waitForTransaction(txHash);  await Sleep(100);  console.log(`New value: ${await getCounter(contractAddress, bob.address())}`); // 获取bob地址下的Counter值,并输出。}if (require.main === module) {  main().then((resp) => console.log(resp));}

执行效果

执行成功,在这里通过 SDK,给一个随机生成的账户 init 了 Counter 资源(Counter=0),然后 incr 了三次,所以最后 Counter 的 Value 为 3

image-20220831200516865

my-counter 相关代码:https://github.com/99Kies/Aptos-Move-Dapp

参考资料

[1]

Using CLI to Run a Local Testnet | Aptos Docs: https://aptos.dev/nodes/local-testnet/using-cli-to-run-a-local-testnet

[2]

源码: https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/move-stdlib/sources/signer.move


通过 DAO,研究组织和媒体可以打破地域的限制,以社区的方式资助和生产内容。DAOrayaki将会通过DAO的形式,构建一个代表社区意志并由社区控制的功能齐全的去中心化媒体。欢迎通过文末方式提交与DAO、量子计算、星际移民、DA相关的内容,瓜分10000USDC赏金池!欢迎加入DAOrayaki社区,了解去中心化自治组织(DAO),探讨最新话题!

官方网站:https://daorayaki.org

Media:https://media.daorayaki.org

Discord server: https://discord.gg/wNUPmsGsa4

Medium: https://medium.com/@daorayaki

Email: daorayaki@dorafactory.org

Twitter: @daorayaki_

微信助手:DAOrayaki-Media

小宇宙:DAOrayaki

详情请参考:

Dora Factory支持去中心化DAO研究组织DAOrayaki

对DAOrayaki第一阶段的回顾--去中心化媒体的先驱

DAOrayaki |DAOrayaki 开启去中心化治理2.0时代

DAOrayaki |风险投资的范式转移:无限主义基金和无限游戏

DAOrayaki |DAOrayaki dGov 模型:基于Futarchy的正和游戏

更多关于DAO的文章,关注Dorafactory,查看往期文章。

DAOrayaki

DAOrayaki is a decentralized media and research organization that is autonomous by readers, researchers, and funders.