BTC
ETH
HTX
SOL
BNB
查看行情
简中
繁中
English
日本語
한국어
ภาษาไทย
Tiếng Việt

DAOrayaki:Aptos & Move實操講解

DAOrayaki
特邀专栏作者
2022-09-21 03:19
本文約13659字,閱讀全文需要約20分鐘
本文主要講解操作aptos cli 和aptos sdk。
AI總結
展開
本文主要講解操作aptos cli 和aptos sdk。

DAOrayaki DAO研究獎金池:

資助地址: DAOrayaki.eth

投票進展:DAO Reviewer 3/0 通過

研究種類:Aptos,Layer1

創作者:FF@DoraFactory

本文主要講解操作aptos cli 和aptos 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]

  • 啟動本地鏈

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/6181
Aptos is running, press ctrl-c to exit
Faucet is running.  Faucet endpoint: 0.0.0.0:8081

啟動成功後會提示rest api 和faucet api 的地址。後面需要把這兩個信息配置在aptos cli 環境內。

  • 配置aptos cli 環境

為了通過命令行訪問和調用本地測試鏈,我們需要給aptos cli 根據上面的部署信息配置config。

PROFILE=local
aptos init --profile $PROFILE --rest-url  --faucet-url 

執行過程中,我們會得到如下的輸出。我們可以選擇輸入一個秘鑰,也可以默認隨機生成。

Configuring for profile local
Using 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 coins
Aptos is now set up for account 7100C5295ED4F9F39DCC28D309654E291845984518307D3E2FE00AEA5F8CACC1!  Run `aptos help` for more information about commands
{
  "Result": "Success"
}

從現在開始,我們就可以通過添加--profile local命令以在本地測試網上運行它們。

ps: 這裡的--profile,就像是k8s 裡的kube-config,可以設置不同的profile 環境,控制不同的網絡。

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 local
aptos 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 的限制

module M {
    struct T has key, store {
        field: u8
    }
}Resource 的限制

在代碼中,Resource 類型有幾個主要限制:

  • Resource 存儲在帳戶下。因此,只有在分配帳戶後才會存在,並且只能通過該帳戶訪問。

  • 一個帳戶同一時刻只能容納一個某類型的Resource。

  • Resource 不能被複製;與它對應的是一種特殊的kind:resource,它與copyable不同,這一點在泛型章節中已經介紹。 (這裡可以抽像到Rust 的所有權那)

  • 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 devtest
aptos 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 devtest
aptos move run --function-id :::: --profile devtest
# 列出指定賬戶的modules/resources信息
aptos account list --query modules --account 0xa1285adb4b8abedf5faf7a46d260c5844f1f64d59dd9b8869db1543cf5bbadf4 --profile devtest
aptos account list --query resources --account 0x4200c2b801870f20a709abba80b6edb90a45ecd9b8acce9842b93d597602edcf --profile devtest

Aptos SDK 調用Move 合約

編譯好合約之後,我們可以通過sdk 調用我們的合約。

我們可以選擇通過sdk 部署合約,也可以通過sdk 調用move 合約。

  • 通過sdk 部署合約

當我們編譯完成之後,會在move 合約文件夾下生成build/ 文件夾

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

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

  • 部署合約相關的sdk 代碼

/** Publish a new module to the blockchain within the specified account */
export async function publishModule(accountFrom: AptosAccount, moduleHex: string): Promise {
  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;
}

  • 通過SDK 發送交易

這裡,我們以my-counter 合約中的init_counter 和incr_counter 為例。

構造兩個方法用於調用這兩個方法,從而實現客戶端調用init 和incr 的功能。

async function initCounter(contractAddress: string, accountFrom: AptosAccount): Promise {
  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 {
  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;
}

  • 通過SDK 獲取賬戶裡的資源信息。

resource 是存放在所屬的賬戶地址下的,我們可以根據account 地址,查詢相關的resource 信息。

getCounter()方法其實就是獲取my-counter 下的**Counter **資源。


async function getCounter(contractAddress: string, accountAddress: MaybeHexString): Promise {
  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((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));
}

執行效果

參考資料

image-20220831200516865

參考資料

參考資料

[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


Aptos
歡迎加入Odaily官方社群