明星公鏈Aptos初體驗--傳送交易和構建合約

語言: CN / TW / HK

Aptos簡介,構造交易和合約的樣例

Aptos作為有背景有技術的新公鏈最近可謂吸滿了投資,賺足了眼球。其從Libra和Diem繼承來的技術又支援其超高tps和高安全高穩定的合約機制,官網上明晃晃的"Building the safest and most scalable Layer 1 blockchain."彰顯著這群前FB工程師的野心。我們今天便淺嘗一下這個技術和資本共同的寵兒,Aptos和Move語言。

Aptos是一條不相容evm的Layer1公鏈,其特點是高安全性,高穩定性,高擴充套件性以及高達100k+的恐怖tps。獨特的儲存模型使其可能在NFT和GameFi領域大展身手。合約語言為Move,是一個基於Rust的記憶體安全型合約語言,在Libra時期便已經成型,目前Sui和Aptos都在使用它構造"有史以來最高效能的公鏈"。據社交媒體透露,現在Move語言開發者的工資時薪已經高達$1200,這讓我不禁留下了悔恨的口水,哦不是,淚水。

這篇文章將從頭到尾體驗一下在Aptos鏈上發起交易和編寫合約,不需要編寫程式碼。通過執行,測試aptos官方事例,閱讀 move程式碼,感受這個號稱最安全Layer1鏈的魅力。官方事例給出了TypeScript,Rust,和Python三種語言的程式碼,考慮到aptos-core本身是由Rust編寫而且其合約語言Move又與Rust極度相似,本文使用Rust事例進行講解。

0.準備工作

安裝rust,不多做介紹

下載aptos-core程式碼庫 git clone https://github.com/aptos-labs/aptos-core.git

進入程式碼庫 cd aptos-core

切換分支 git checkout --track origin/devnet

執行啟動指令碼,構建開發環境 ./scripts/dev_setup.sh

下載Aptos Commandline tool命令列工具 cargo install --git https://github.com/aptos-labs/aptos-core.git aptos

1.發起交易

這一部分程式碼在 aptos-core/developer-docs-site/static/examples/rust/first_transaction 裡:

1.1 建立賬戶

關於aptos的賬戶系統詳情可以看官方文件: https://aptos.dev/concepts/basics-accounts

pub struct Account {
    signing_key: SecretKey,
}

impl Account {
    /// Represents an account as well as the private, public key-pair for the Aptos blockchain.
    pub fn new(priv_key_bytes: Option<Vec<u8>>) -> Self {
        let signing_key = match priv_key_bytes {
            Some(key) => SecretKey::from_bytes(&key).unwrap(),
            None => {
                let mut rng = rand::rngs::StdRng::from_seed(OsRng.gen());
                let mut bytes = [0; 32];
                rng.fill_bytes(&mut bytes);
                SecretKey::from_bytes(&bytes).unwrap()
            }
        };

        Account { signing_key }
    }
    /// Returns the address associated with the given account
    pub fn address(&self) -> String {
        self.auth_key()
    }

    /// Returns the auth_key for the associated account
    pub fn auth_key(&self) -> String {
        let mut sha3 = Sha3::v256();
        sha3.update(PublicKey::from(&self.signing_key).as_bytes());
        sha3.update(&vec![0u8]);

        let mut output = [0u8; 32];
        sha3.finalize(&mut output);
        hex::encode(output)
    }

    /// Returns the public key for the associated account
    pub fn pub_key(&self) -> String {
        hex::encode(PublicKey::from(&self.signing_key).as_bytes())
    }
}

1.2 準備一個REST介面包裝器

​ 構造一個RestClient,並連線測試網 https://fullnode.devnet.aptoslabs.com

#[derive(Clone)]
pub struct RestClient {
    url: String,
}

///RestClient中的方法
impl RestClient {

    //從url初始化client
    pub fn new(url: String) -> Self {
        Self { url }
    }

    ///Rest請求的具體實現,下邊詳細講解
    .......
}

1.2.1 讀取賬戶資訊

以下程式碼是通過賬戶地址讀取賬戶資訊的介面實現

值得注意的是account_resource介面,aptos的任何賬戶都有data儲存,可以用來儲存貨幣/NFT等等,這些資訊被稱為resource。

如果我們要查詢某個賬戶的AptosCoin餘額,就要定位到 0x1::coin::CoinStore&lt;0x1::aptos_coin::AptosCoin ,查詢對應的resource資料,在1.2.3中account_balance就是複用了該介面。

這裡的0x1實際上是一個account地址,因為AptosCoin是root賬戶0x1發行的,所以會出現0x1這種寫法,類似寫法在下文也會出現。而coin::CoinStore<>是aptos對代幣的resource的一種特殊處理,以提供安全性。這樣的話可以理解為查詢該賬戶下,由0x1發行的AptosCoin的數目。

詳情見aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/sources/coin.move

/// 返回賬戶的私鑰和序列碼sequence_number,詳情見https://aptos.dev/concepts/basics-accounts
    pub fn account(&self, account_address: &str) -> serde_json::Value {
        let res =
            reqwest::blocking::get(format!("{}/accounts/{}", self.url, account_address)).unwrap();

        if res.status() != 200 {
            assert_eq!(
                res.status(),
                200,
                "{} - {}",
                res.text().unwrap_or("".to_string()),
                account_address,
            );
        }

        res.json().unwrap()
    }

    /// 返回賬戶所有相關資訊
    pub fn account_resource(
        &self,
        account_address: &str,
        resource_type: &str,
    ) -> Option<serde_json::Value> {
        let res = reqwest::blocking::get(format!(
            "{}/accounts/{}/resource/{}",
            self.url, account_address, resource_type,
        ))
        .unwrap();

        if res.status() == 404 {
            None
        } else if res.status() != 200 {
            assert_eq!(
                res.status(),
                200,
                "{} - {}",
                res.text().unwrap_or("".to_string()),
                account_address,
            );
            unreachable!()
        } else {
            Some(res.json().unwrap())
        }
    }

1.2.2 交易相關操作(生成,簽名,提交)

/// Generates a transaction request that can be submitted to produce a raw transaction that can be signed, which upon being signed can be submitted to the blockchain.
    pub fn generate_transaction(
        &self,
        sender: &str,
        payload: serde_json::Value,
    ) -> serde_json::Value {
        let account_res = self.account(sender);

        let seq_num = account_res
            .get("sequence_number")
            .unwrap()
            .as_str()
            .unwrap()
            .parse::<u64>()
            .unwrap();

        // Unix timestamp, in seconds + 10 minutes
        let expiration_time_secs = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("Time went backwards")
            .as_secs()
            + 600;

        serde_json::json!({
            "sender": format!("0x{}", sender),
            "sequence_number": seq_num.to_string(),
            "max_gas_amount": "1000",
            "gas_unit_price": "1",
            "gas_currency_code": "XUS",
            "expiration_timestamp_secs": expiration_time_secs.to_string(),
            "payload": payload,
        })
    }

    /// Converts a transaction request produced by `generate_transaction` into a properly signed transaction, which can then be submitted to the blockchain.
    pub fn sign_transaction(
        &self,
        account_from: &mut Account,
        mut txn_request: serde_json::Value,
    ) -> serde_json::Value {
        let res = reqwest::blocking::Client::new()
            .post(format!("{}/transactions/signing_message", self.url))
            .body(txn_request.to_string())
            .send()
            .unwrap();

        if res.status() != 200 {
            assert_eq!(
                res.status(),
                200,
                "{} - {}",
                res.text().unwrap_or("".to_string()),
                txn_request.as_str().unwrap_or(""),
            );
        }
        let body: serde_json::Value = res.json().unwrap();
        let to_sign_hex = Box::new(body.get("message").unwrap().as_str()).unwrap();
        let to_sign = hex::decode(&to_sign_hex[2..]).unwrap();
        let signature: String = ExpandedSecretKey::from(&account_from.signing_key)
            .sign(&to_sign, &PublicKey::from(&account_from.signing_key))
            .encode_hex();

        let signature_payload = serde_json::json!({
            "type": "ed25519_signature",
            "public_key": format!("0x{}", account_from.pub_key()),
            "signature": format!("0x{}", signature),
        });
        txn_request
            .as_object_mut()
            .unwrap()
            .insert("signature".to_string(), signature_payload);
        txn_request
    }

    /// Submits a signed transaction to the blockchain.
    pub fn submit_transaction(&self, txn_request: &serde_json::Value) -> serde_json::Value {
        let res = reqwest::blocking::Client::new()
            .post(format!("{}/transactions", self.url))
            .body(txn_request.to_string())
            .header("Content-Type", "application/json")
            .send()
            .unwrap();

        if res.status() != 202 {
            assert_eq!(
                res.status(),
                202,
                "{} - {}",
                res.text().unwrap_or("".to_string()),
                txn_request.as_str().unwrap_or(""),
            );
        }
        res.json().unwrap()
    }

    /// Submits a signed transaction to the blockchain.
    pub fn execution_transaction_with_payload(
        &self,
        account_from: &mut Account,
        payload: serde_json::Value,
    ) -> String {
        let txn_request = self.generate_transaction(&account_from.address(), payload);
        let signed_txn = self.sign_transaction(account_from, txn_request);
        let res = self.submit_transaction(&signed_txn);
        res.get("hash").unwrap().as_str().unwrap().to_string()
    }

    pub fn transaction_pending(&self, transaction_hash: &str) -> bool {
        let res = reqwest::blocking::get(format!("{}/transactions/{}", self.url, transaction_hash))
            .unwrap();

        if res.status() == 404 {
            return true;
        }

        if res.status() != 200 {
            assert_eq!(
                res.status(),
                200,
                "{} - {}",
                res.text().unwrap_or("".to_string()),
                transaction_hash,
            );
        }

        res.json::<serde_json::Value>()
            .unwrap()
            .get("type")
            .unwrap()
            .as_str()
            .unwrap()
            == "pending_transaction"
    }

    /// Waits up to 10 seconds for a transaction to move past pending state.
    pub fn wait_for_transaction(&self, txn_hash: &str) {
        let mut count = 0;
        while self.transaction_pending(txn_hash) {
            assert!(count < 10, "transaction {} timed out", txn_hash);
            thread::sleep(Duration::from_secs(1));
            count += 1;
        }
    }

1.2.3 構造交易邏輯

一個是account_balance,使用account_resource呼叫AptosCoin資源查詢 另一個是transfer,使用的是 0x1::coin::transfer 也就是coin共有方法transfer

/// Returns the test coin balance associated with the account
    pub fn account_balance(&self, account_address: &str) -> Option<u64> {
        self.account_resource(
            account_address,
            "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>",
        )
        .unwrap()["data"]["coin"]["value"]
            .as_str()
            .and_then(|s| s.parse::<u64>().ok())
    }

    /// Transfer a given coin amount from a given Account to the recipient's account address.
    /// Returns the sequence number of the transaction used to transfer
    pub fn transfer(&self, account_from: &mut Account, recipient: &str, amount: u64) -> String {
        let payload = serde_json::json!({
            "type": "script_function_payload",
            "function": "0x1::coin::transfer",
            "type_arguments": ["0x1::aptos_coin::AptosCoin"],
            "arguments": [format!("0x{}", recipient), amount.to_string()]
        });
        let txn_request = self.generate_transaction(&account_from.address(), payload);
        let signed_txn = self.sign_transaction(account_from, txn_request);
        let res = self.submit_transaction(&signed_txn);

        res.get("hash").unwrap().as_str().unwrap().to_string()
    }
}

1.3 準備一個水龍頭介面包裝器

pub struct FaucetClient {
    url: String,
    rest_client: RestClient,
}

impl FaucetClient {

    /// 水龍頭可以建立賬戶並給其分配資產,這是一個包裝器
    pub fn new(url: String, rest_client: RestClient) -> Self {
        Self { url, rest_client }
    }

    /// 給傳入使用者鑄幣
    pub fn fund_account(&self, auth_key: &str, amount: u64) {
        let res = reqwest::blocking::Client::new()
            .post(format!(
                "{}/mint?amount={}&auth_key={}",
                self.url, amount, auth_key
            ))
            .send()
            .unwrap();

        if res.status() != 200 {
            assert_eq!(
                res.status(),
                200,
                "{}",
                res.text().unwrap_or("".to_string()),
            );
        }
        for txn_hash in res.json::<serde_json::Value>().unwrap().as_array().unwrap() {
            self.rest_client
                .wait_for_transaction(txn_hash.as_str().unwrap())
        }
    }
}

1.4 執行測試

fn main() -> () {
    let rest_client = RestClient::new(TESTNET_URL.to_string());
    let faucet_client = FaucetClient::new(FAUCET_URL.to_string(), rest_client.clone());

    //建立兩個賬戶Alice和Bob,並用水龍頭給Alice賺一筆賬
    let mut alice = Account::new(None);
    let bob = Account::new(None);

    println!("\n=== Addresses ===");
    println!("Alice: 0x{}", alice.address());
    println!("Bob: 0x{}", bob.address());

    faucet_client.fund_account(&alice.auth_key().as_str(), 1_000_000);
    faucet_client.fund_account(&bob.auth_key().as_str(), 0);

    //呼叫account_balance查詢賬戶餘額
    println!("\n=== Initial Balances ===");
    println!("Alice: {:?}", rest_client.account_balance(&alice.address()));
    println!("Bob: {:?}", rest_client.account_balance(&bob.address()));

    // Alice構造一筆向bob轉賬1000的交易並提交等待完成
    let tx_hash = rest_client.transfer(&mut alice, &bob.address(), 1_000);
    rest_client.wait_for_transaction(&tx_hash);

    //呼叫account_balance查詢賬戶餘額
    println!("\n=== Final Balances ===");
    println!("Alice: {:?}", rest_client.account_balance(&alice.address()));
    println!("Bob: {:?}", rest_client.account_balance(&bob.address()));
}

執行 cargo run --bin first-transaction (執行前確保您在 aptos-core/developer-docs-site/static/examples/rust 目錄下)

1.5 輸出

可以看到轉賬成功後Alice和Bob的餘額(去掉gas費)

=== Addresses ===
Alice: e26d69b8d3ff12874358da6a4082a2ac
Bob: c8585f009c8a90f22c6b603f28b9ed8c

=== Initial Balances ===
Alice: 1000000000
Bob: 0

=== Final Balances ===
Alice: 999998957
Bob: 1000

2.玩轉合約

aptos鏈使用Move語言編寫合約,其特點是安全穩定,語法上與Rust很像。

我們現在構建一個新的合約,在aptos的世界裡稱為module。

需要完成以下幾個步驟:

1.編寫,編譯,測試module 2.部署module 3.與module的資源(儲存區)互動

2.1 閱讀合約程式碼

我們先進到 aptos-move/move-examples/hello_blockchain 目錄裡,我們暫且稱其為“Move目錄”,方便之後切換目錄指稱。

在這個目錄裡我們可以看到這個 sources/HelloBlockchain.move 檔案,這個module可以讓賬戶可以建立並修改一個String型別的資源,每個使用者都只能操作自己的資源。

module HelloBlockchain::Message {
    use std::string;
    use std::error;
    use std::signer;

    struct MessageHolder has key {
        message: string::String,
    }

    public entry fun set_message(account: signer, message_bytes: vector<u8>)
    acquires MessageHolder {
        let message = string::utf8(message_bytes);
        let account_addr = signer::address_of(&account);
        if (!exists<MessageHolder>(account_addr)) {
            move_to(&account, MessageHolder {
                message,
            })
        } else {
            let old_message_holder = borrow_global_mut<MessageHolder>(account_addr);
            old_message_holder.message = message;
        }
    }
}

在上述程式碼中有兩個關鍵,一個是結構體 MessageHolder 一個是函式 set_messageset_message 是一個script函式,允許被交易直接呼叫,呼叫它之後函式會確認賬戶是否有 MessageHolder 資源,沒有的話就建立一個並把資訊寫入,有的話就覆蓋掉。

2.2測試合約

Move測試可以直接寫在合約裡,我們加上了一個sender_can_set_message測試函式,用cargo test進行測試。

執行 cargo test test_hello_blockchain -p move-examples -- --exact 即可。

#[test(account = @0x1)]
    public(script) fun sender_can_set_message(account: signer) acquires MessageHolder {
        let addr = Signer::address_of(&account);
        set_message(account,  b"Hello, Blockchain");

        assert!(
          get_message(addr) == string::utf8(b"Hello, Blockchain"),
          0
        );
    }

2.3部署合約

現在我們回到之前transaction樣例的同級目錄,找到 developer-docs-site/static/examples/rust/hello_blockchain 檢視部署和互動module的程式碼。這會複用一些上一節的函式。這一節我們只討論新功能,比如部署module, set_message 交易,以及讀取 MessageHolder::message 資源,部署module和提交交易的區別就只有payload,我們開始看吧:

2.3.1 部署module

pub struct HelloBlockchainClient {
    pub rest_client: RestClient,
}

impl HelloBlockchainClient {
    /// Represents an account as well as the private, public key-pair for the Aptos blockchain.
    pub fn new(url: String) -> Self {
        Self {
            rest_client: RestClient::new(url),
        }
    }

    /// Publish a new module to the blockchain within the specified account
    pub fn publish_module(&self, account_from: &mut Account, module_hex: &str) -> String {
        let payload = serde_json::json!({
            "type": "module_bundle_payload",
            "modules": [{"bytecode": format!("0x{}", module_hex)}],
        });
        self.rest_client
            .execution_transaction_with_payload(account_from, payload)
    }

2.3.2 讀取資源

Module 釋出在一個地址上,就是下邊的 contract_address 。上一節轉移Coin時候的0x1也是釋出地址。

/// Retrieve the resource Message::MessageHolder::message
    pub fn get_message(&self, contract_address: &str, account_address: &str) -> Option<String> {
        let module_type = format!("0x{}::Message::MessageHolder", contract_address);
        self.rest_client
            .account_resource(account_address, &module_type)
            .map(|value| value["data"]["message"].as_str().unwrap().to_string())
}

2.3.3 修改資源

Module必須暴露出script函式才能初始化和修改資源,script可以被交易呼叫。

/// Potentially initialize and set the resource Message::MessageHolder::message
    pub fn set_message(
        &self,
        contract_address: &str,
        account_from: &mut Account,
        message: &str,
    ) -> String {
        let message_hex = hex::encode(message.as_bytes());
        let payload = serde_json::json!({
            "type": "script_function_payload",
            "function": format!("0x{}::Message::set_message", contract_address),
            "type_arguments": [],
            "arguments": [message_hex]
        });
        self.rest_client
            .execution_transaction_with_payload(account_from, payload)
    }

2.4 初始化並互動

進入 developer-docs-site/static/examples/rust ,我們姑且稱為"App 目錄"

執行 cargo run --bin hello-blockchain -- Message.mv

過了一會,控制檯會輸出Alice與Bob的賬戶資訊並顯示 Update the module with Alice's address, build, copy to the provided path, and press enter. ,記錄下Alice的地址,不要關閉

這時我們另起一個控制檯,進入"Move目錄",將 hello_blockchain/move.toml 中的 HelloBlockChain='_' 配置為Alice地址。

執行 aptos move compile --package-dir . --named-addresses HelloBlockchain=0x{Alice的地址}

Module編譯成功,將 build/Examples/bytecode_modules/Message.mv 複製一份到 developer-docs-site/static/examples/rust

在"App 目錄"的控制檯輸入回車讓它繼續執行

輸出如果類似這樣就是成功了:

=== Addresses ===
Alice: 11c32982d04fbcc79b694647edff88c5b5d5b1a99c9d2854039175facbeefb40
Bob: 7ec8f962139943bc41c17a72e782b7729b1625cf65ed7812152a5677364a4f88

=== Initial Balances ===
Alice: 10000000
Bob: 10000000

Update the module with Alice's address, build, copy to the provided path, and press enter.

=== Testing Alice ===
Publishing...
Initial value: None
Setting the message to "Hello, Blockchain"
New value: Hello, Blockchain

=== Testing Bob ===
Initial value: None
Setting the message to "Hello, Blockchain"
New value: Hello, Blockchain

證明了Alice和Bob都新建立了Message資源並置為"Hello, Blockchain"

3.相關資料

Aptos 官方文件: https://aptos.dev Move手冊: https://move-language.github.io/move/ 區塊鏈瀏覽器: https://explorer.devnet.aptos.dev api文件: https://fullnode.devnet.aptoslabs.com/spec.html #/

本文參與登鏈社群寫作激勵計劃 ,好文好收益,歡迎正在閱讀的你也加入。

  • 發表於 2分鐘前
  • 閱讀 ( 2 )
  • 學分 ( 0 )
  • 分類:公鏈