What is rustweb3
rustweb3 是一个致力于推广 rust 和 web3 的 电子小书。
终极愿景是:
👍 All in rust 🦀 , all in web3 ! 👍
电子书主要结构
- 使用 rust 进行 solana 开发
- 使用 rust 进行 evm 开发
- 使用 rust 进行 sui 开发
- 使用 rust 进行 aptos 开发
- 使用 rust 进行 rooch 开发
- 使用 rust 进行 bitcoin 开发
现有项目:
这是一个快速开发 rust 应用的框架模板,整个项目中涉及到的所有应用都是由这个模板产生。内部封装了一些常用的 cli 基本功能接口,供外部使用。
这是一个 cargo 的插件,用于快速构建 web3 的应用。
dotenv
我们在开发过程中,需要使用到一些敏感信息,比如私钥、助记词、API 密钥等。
这些信息如果直接写在代码中,可能会导致信息泄露,造成不必要的损失。
一般情况下,我们会将这些信息写在 .env
文件中,然后在代码中加载这些信息。
同时,这个 .env, 要被我们加在 .gitignore 、.dockerignore 等这些同步的配置文件中,避免将敏感信息提交或同步到其他的仓库中。
安装
cargo add dotenv
代码初始化加载
默认从 .env 加载,也可以使用 from_path 指定路径加载。
use dotenv::dotenv; use std::path::Path; fn main() { // 默认加载 .env 文件 dotenv().ok(); // 从指定路径加载 const path = Path::new("./path/to/.env"); dotenv::from_path(path).ok(); }
获取变量的值
获取值,默认的得到的是 string 类型。
#![allow(unused)] fn main() { let api_key = std::env::var("API_KEY").expect("API_KEY must be set"); }
不过,可以使用 unwrap 的串联,转化数据类型。
#![allow(unused)] fn main() { let ok: bool = env::var("ok").unwrap().parse().unwrap(); }
使用实例
use dotenv::dotenv; fn main() { dotenv().ok(); // 必须包含的变量 let api_key = std::env::var("API_KEY").expect("API_KEY must be set"); println!("API_KEY: {}", api_key); // 指定默认值 let port = std::env::var("PORT").unwrap_or_else(|_| "3000".to_string()); println!("PORT: {}", port); let ok: bool = env::var("ok").unwrap().parse().unwrap(); println!("ok -> : {}", ok); }
生成助记词
聊到 web3,就不得不说一个重要概念: 助记词。 通过助记词,通过秘钥派生算法,可以完成钱包子账户的派生。 完全可以这么说: 控制一个助记词,等于控制了钱包中所有的账户。
助记词一般为一组单词,词库由 BIP39 提供。 单词的长度可以为 12 个,15 个,18 个,21 个,24 个。
安装
rust 中,通过 bip39
的 crate 可以完成助记词的生成和使用。根据不同的语言,需要开启不同的 features。
比如:
- 中文简体:
chinese-simplified
- 繁体中文:
chinese-traditional
同时为了,可以随机生成助记词,需要开启 rand_core
的 feature。
cargo add bip39 --features "chinese-simplified rand_core"
cargo add rand
生成助记词
助记词生成,需要一个随机数发生器。指定语言,指定助记词长度即可。
use bip39::{Language, Mnemonic}; fn main() { let mut rng = rand::thread_rng(); let words = Mnemonic::generate_in_with(&mut rng, Language::English, 12).unwrap(); println!("{}", words); }
助记词导入及使用
助记词导入,直接使用 from_str
方法即可。使用的时候,可以指定一个 password 来增加安全性。
#![allow(unused)] fn main() { let words = Mnemonic::from_str( &"rural soup rose assist derive isolate lobster receive seek guilt verify glow", ) .unwrap(); println!("{:?}", words); let seeds = words.to_seed("abc"); println!("{:?}", hex::encode(seeds)); }
大多数的 web 钱包都会使用助记词的方式来管理秘钥。 但是,考虑到导入助记词过程的复杂性,很多钱包都直接使用了 空的 password 来导入助记词。
bip32 & bip44 & slip10
通过上一节 bip39 中定义的助记词,我们可以产生一个种子。种子可以派生秘钥,秘钥可以派生地址。 这样,web3 的分层钱包系统也就建立起来了。
bip32
最开始提出的是 bip32 的标准,通过递归的方式,可以产生一个秘钥树。 但是, BIP32 并没有定义明确的路径结构,路径只要符合一个简单的字符串即可。 比如: m/0/1/2 各模块之间可以随意定义,所以,如果在一个钱包中支持多币种,BIP32 没有提供一个可靠的方案。 为了解决这个问题,提出了 bip44 的标准,给各个模块定义了标准。
bip44
所以,在 bip32 的基础上,提出了 bip44 的标准。同时,也给出了各个模块的定义标准:
一个实例 : m/44’/60’/0’/0/0
各个部分定义如下:
- m/44’ 表示使用 bip44 标准
- 60’ 表示使用以太坊
- 0’ 表示使用主网
- 0 表示使用第一个账户
- 0 表示使用第一个地址
程序实例
通过 bip39 的助记词,借助 bip44 标准,派生 evm 地址
安装基础类库:
cargo add bip32
cargo add sha3
以下是一个,通过 bip44 派生 evm 地址的实例。
use { bip32::{DerivationPath, PublicKey, XPrv}, bip39::Mnemonic, hex, sha3::Digest, std::str::FromStr, }; fn main() { // let mut rng = rand::thread_rng(); // let words = Mnemonic::generate_in_with(&mut rng, Language::English, 12).unwrap(); let words = Mnemonic::from_str( "giant fever unveil bench mass tourist green spoon song scissors goat thumb", ) .unwrap(); println!("current words: {}", words.to_string()); // use the words to generate a evm address let seeds = words.to_seed(""); // let root_xprv = XPrv::new(&seeds).unwrap(); let evm_derive_path = DerivationPath::from_str("m/44'/60'/0'/0/0").unwrap(); // Xprv is ExtendedPrivateKey<k256::ecdsa::SigningKey>, so you can get it by calling private_key() let evm_xprv = XPrv::derive_from_path(seeds, &evm_derive_path).unwrap(); let evm_public_key = evm_xprv.private_key().verifying_key(); println!("evm public key: {}", hex::encode(evm_public_key.to_bytes())); let mut kh = sha3::Keccak256::new(); kh.update(&evm_public_key.to_encoded_point(false).as_bytes()[1..]); let hash = kh.finalize().to_vec(); let evm_address = &hash[12..]; println!("evm address: 0x{}", hex::encode(evm_address)); }
部分代码说明:
- DerivationPath::from_str 可以定义推导路径,这部分,不同的币种有不同的推导路径。
- 通过 XPrv::derive_from_path 可以派生出私钥,这部分是一组扩展秘钥,可以转化为各种需要的秘钥格式。实例中 Xprv 是 ExtendedPrivateKeyk256::ecdsa::SigningKey 类型,所以,可以调用 private_key() 获取私钥。
- 通过 private_key() 可以获取私钥,通过 verifying_key() 可以获取公钥。
- 通过公钥,进行 keccak256 哈希运算,获取 evm 地址。
SLIP10
很多公链(比如 SUI、Aptos、Solana)采用的是 ed25519 算法,原始的 bip32 并没有定义 ed25519 的秘钥派生方式。 SLIP10 是 BIP32 的改进版,定义了 ed25519 的秘钥派生方式。生成代码逻辑如下:
#![allow(unused)] fn main() { use bip32::DerivationPath; use hmac::{Hmac, Mac}; use sha2::Sha512; pub fn derive_ed25519_private_key_by_path(seed: &[u8], path: DerivationPath) -> [u8; 32] { let indexes = path .into_iter() .map(|i: bip32::ChildNumber| i.into()) .collect::<Vec<_>>(); derive_ed25519_private_key(seed, &indexes) } #[allow(non_snake_case)] fn derive_ed25519_private_key(seed: &[u8], indexes: &[u32]) -> [u8; 32] { let mut I = hmac_sha512(b"ed25519 seed", &seed); let mut data = [0u8; 37]; for i in indexes { let hardened_index = 0x80000000 | *i; let Il = &I[0..32]; let Ir = &I[32..64]; data[1..33].copy_from_slice(Il); data[33..37].copy_from_slice(&hardened_index.to_be_bytes()); //I = HMAC-SHA512(Key = Ir, Data = 0x00 || Il || ser32(i')) I = hmac_sha512(&Ir, &data); } I[0..32].try_into().unwrap() } pub fn hmac_sha512(key: &[u8], data: &[u8]) -> [u8; 64] { type HmacSha512 = Hmac<Sha512>; let mut hmac = HmacSha512::new_from_slice(key).expect("HMAC can take key of any size"); hmac.update(data); let result = hmac.finalize(); result.into_bytes().try_into().unwrap() } }
hash & encode
encode
hex
先简单介绍编码,编码可以把字节流(所有数据都可以是字节形式)显示成可视字符。 web3 世界中大部分数据操作 (hash,signature) 也是以字节为基础的。 这就使得,数据的可读性差一些。引入编码,就可以解决这个问题, 其中以 hex 编码居多。
安装 hex 编码库
cargo add hex
一个基本的 encode 、decode 示例
fn main() { let msg = b"Hello, world!"; let result = hex::encode(msg); println!("{}", &result); let msg_x = hex::decode(result).unwrap(); println!("{}", String::from_utf8(msg_x).unwrap()); }
将字节数组还原成 string 的时候,需要使用 String::from_utf8(msg_x).unwrap()
来还原。
同时,因为结果的不确定性,所以,会返回一个 Result 类型。
bcs
bcs 编码是一个二进制编码,可以把一个 满足要求的 结构体编码成字节数组,同时,也可以将一个字节流反序列化成一个结构体。 方便数据安全高效的传递,web3 中会用到 bcs 传递参数和返回结果。
安装 bcs 编码库
cargo add bcs
cargo add serde
use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] struct Payload { value: u8, enabled: bool, msg: String, } fn main() { let p = Payload { value: 123, enabled: false, msg: "hello world".to_string(), }; let d = bcs::to_bytes(&p).unwrap(); println!("bcs encode {}", hex::encode(&d)); let x = bcs::from_bytes::<Payload>(&d).unwrap(); println!("value is : {}", x.value); }
需要序列化的结构体,必须满足 Serialize,需要反序列化的结构体,必须满足 Deserialize 的 trait 。
可以通过 serde::{Deserialize, Serialize};
的宏来快速实现。
hash
hash 是 web3 中常用的操作,用于将数据转换为固定长度的字节数组。 一般用来唯一识别数据的完整性。
简单的 sha hash , 包含: sha256, sha384, sha512 等。
使用通用的类库 sha2 来实现。
cargo add sha2
其中 sha2 的 crates 中包含 sha256, sha384, sha512 等。 因,Hash 计算,大多数会返回不可读的字节数组,所以,一般都会和 hex 编码一起使用。
下边是一个计算 sha256 的示例
use sha2::Digest; fn main() { let msg = "Hello, world!"; let mut h = sha2::Sha256::new(); h.reset(); h.update(msg); let hash = h.finalize(); println!("{}", hex::encode(hash)); }
计算 Hash 之前,需要生成对应的编码器 : let mut h = sha2::Sha256::new(), 同时编码器,必须包含 mut 属性。 然后,将需要计算的数据写入编码器中,最后,调用 finalize 方法,获取计算结果。
如果 Hash 计算器,需要多次计算。那么,需要先调用 reset 方法,重置编码器。
hmac
在 Sha hash 的基础上,附加一个 key ,除了完成自身的数据完整性以外,还可以添加一层简单的身份认证。 这种算法,在身份认证方面,也应用较为广泛,数据发送者,与数据传递着、数据接受者之间,可以用一个 key 来做验证。
安装
cargo add hmac
计算 hmac hash 的实例:
#![allow(unused)] fn main() { let mut hx = hmac::Hmac::<sha2::Sha256>::new_from_slice(b"hello").unwrap(); hx.update(b"world"); let b = hx.finalize().into_bytes(); println!("{}", hex::encode(b)); }
指定一个 key , 然后写入数据,通过 finalize 获取对应的字节流。
hmac 在 后续的 slip10 中被用来生成子账号的秘钥。
New Bitcoin Address
Query From Bitcoin
Send Transaction
连接到 solana 网络
安装基本环境
使用 solana 开发程序之前需要一些基础的 sdk,通过 cargo 安装即可。
cargo add solana-client
cargo add solana-sdk
随着版本变化,可能安装的版本不一致。
网络连接
solana 的通用网络分四种
Environment | URL |
---|---|
Localhost | http://127.0.0.1:8899 |
Devnet | https://api.devnet.solana.com |
Testnet | https://api.testnet.solana.com |
Mainnet-Beta | https://api.mainnet-beta.solana.com |
其中 Localhost 网络可以通过 solana-test-validator -r
启动,启动以后,将在本地监听 8899 的 rpc 端口。
使用之前需要提前安装 solana
。
devnet
和 testnet
为两种不同的测试网络。
mainnet
就是主网了,牵扯真金白银的。 所以上主网之前需要在其他的网络测试完整。
网络的操作基本上都是通过 solana_client::rpc_client::RpcClient
来完成的。初始化也极为简单:
#![allow(unused)] fn main() { let client = RpcClient::new("https://api.devnet.solana.com".to_string()); }
一般情况下,你可能需要通过 quicknode 这类第三方 rpc 获得可用性较高的 rpc 节点。
网络请求
初始化好了客户端就可以发起rpc 请求了。 大多数网络请求都遵循 solana 的 rpc 文档。
以下是两个简单的请求,获取 链的版本号和当前区块高度。
#![allow(unused)] fn main() { info!("version : {}", client.get_version().unwrap()); info!("block height : {}", client.get_block_height().unwrap()); }
账号结构
账号部分操作几种在 solana_sdk::signature::{Keypair, Signer}
。
Keypair 其实就是 ed25519_dalek 的一组 Keypir 密钥对, Signer 是对应的 trait 接口。
生成随机账户:
#![allow(unused)] fn main() { let pair: Keypair = Keypair::new(); }
导出私钥,导出编码过的 bytes 数据,solana 中一般使用 base58编码。
#![allow(unused)] fn main() { pair.to_base58_string() }
打印地址, solana 地址即为公钥,所以,直接打印 pair.pubkey 的 base58编码 即可。
#![allow(unused)] fn main() { pair.pubkey.to_string() }
同理的,base58 编码的字节都可以导入,以获得需要操作的账户。同时 私钥的 base58 编码 也可以导入到各个 solana 钱包当中。
#![allow(unused)] fn main() { let a_pair = Keypair::from_base58_string("base58_key"); }
查询余额及转账
通过 rpc
接口 get_balance 可以返回余额。
简单的完整示例
主要完成以下工作:
- 产生一个随机账户,打印对应的地址、私钥等信息
- 通过 私钥信息重新导入,获取账户
- 检查余额,如果余额为 0 ,那么 申请 airdrop (因为环境问题,或者其他的什么问题,可能 airdrop 失败,可以拿本地网络测试)
use { anyhow::{Ok, Result}, clap::Parser, log::{debug, error, info, warn}, solana_client::rpc_client::RpcClient, solana_playground::{app, cli, jobs}, solana_sdk::signature::{Keypair, Signer}, }; #[tokio::main] async fn main() -> Result<()> { app::RunTime::init(); let mut runtime = app::RunTime::new(); runtime.do_init(app::InitOptions { config_merge_env: true, config_merge_cli: true, }); if runtime.cli.name.is_none() && runtime.cli.command.is_none() { error!("Please input the command"); return Err(anyhow::anyhow!("Please input the command")); } match runtime.cli.command { Some(cli::Command::Test {}) => { let client = RpcClient::new("https://api.devnet.solana.com".to_string()); let pair: Keypair = Keypair::new(); let pubkey = pair.pubkey(); warn!("private key is : {}", pair.to_base58_string()); info!("solana address is : {}", pubkey.to_string()); let a_pair = Keypair::from_base58_string("3a1q7fn3QPex7MkjKFpQcwP8TYUqLyZT6CYUshGRHFPj79Sq67KNJBk9tJSgAVXMNnjfLuUMDCX9epE8DAqTEY6Q"); let a_pubkey = a_pair.pubkey(); info!("a_pubkey address is : {}", a_pubkey.to_string()); let balance = client.get_balance(&a_pubkey).unwrap(); info!("balance is : {}", balance); if balance == 0 { let tx = client.request_airdrop(&a_pubkey, 1000000000).unwrap(); info!("tx is : {:?}", tx); let balance = client.get_balance(&a_pubkey).unwrap(); info!("balance is : {}", balance); } } _ => { error!("Please input the command"); cli::Cli::parse_from(&["rustapp", "--help"]); return Err(anyhow::anyhow!("Please input the command")); } } Ok(()) }
Hello World Program
Call Solana Program
Call Solana Program
Connect To Evm RPC node
Call Contract
Compile Contract
Install
Chapter 1
Aptos 开发基础配置
在开始 aptos
开始之前需要安装 sdk。
aptos 的 sdk 安装相对特殊一些,除了安装 dependencies,还需要打对应的 patch 同时配置独立的编译参数。
安装配置 cargo 依赖
可以使用 cargo 命令安装:
cargo add aptos-sdk --git https://github.com/aptos-labs/aptos-core --branch devnet
或者 也可以直接修改 Cargo.toml 文件:
[dependencies]
aptos-sdk = { git = "https://github.com/aptos-labs/aptos-core", branch = "devnet" }
添加 patch
[patch.crates-io]
merlin = { git = "https://github.com/aptos-labs/merlin" }
x25519-dalek = { git = "https://github.com/aptos-labs/x25519-dalek", branch = "zeroize_v1" }
注意: 因为 aptos sdk 依赖了整个的 aptos crates , 所以,第一次安装很慢,情耐心等待。
配置 build 选项
编辑 .cargo/config.toml 如果没有,创建一个,添加 build 选项
写入如下内容:
[build]
rustflags = ["--cfg", "tokio_unstable"]
连接到 Aptos 网络
Aptos 网络官方提供的 RPC 地址被封装在 aptos_sdk::rest_client::AptosBaseUrl
中。
网络设置为官方连接,分为以下几种。
Name | Fullnode URL | Faucet URL |
---|---|---|
Mainnet | https://fullnode.mainnet.aptoslabs.com | N/A |
Devnet | https://fullnode.devnet.aptoslabs.com | https://faucet.devnet.aptoslabs.com |
Testnet | https://fullnode.testnet.aptoslabs.com | https://faucet.testnet.aptoslabs.com |
Custom | Custom URL provided by the user | N/A |
通过 aptos_sdk::rest_client::Client
提供连接对象,提供 Url 参数。
#![allow(unused)] fn main() { let client = Client::new(AptosBaseUrl::Testnet.to_url()); }
client 提供 rpc 信息,其中有几个信息,你会很快用到
- 链的 ID :
chain_id
#![allow(unused)] fn main() { let index_info = client.get_index().await?; info!("chain_id info: {:?}", index_info.inner().chain_id); }
- 账户
sequence_number
#![allow(unused)] fn main() { let sequence = client.get_account(import_account.address()).await?; info!("account sequence is : {}",sequence.inner().sequence_number); }
这一点 aptos
和其他的公链有所区别。如果是新的账户,那么 sequence_number 则无法获取。同时这个账户也无法进行任何的操作。
解锁办法,往其中发送任意的 apt 激活即可。
产生随机账户
一般随机产生秘钥,然生产生随机账号用于测试。 导入 rand 和 rand_core 的时候,建议和 aptos 官方使用的一致。可以在 sdk 的 Cargo.toml 中查看 。 目前的版本是:
rand = "0.7.3"
rand_core = "0.5.1"
账号操作集中在 aptos_sdk::types::LocalAccount
中,这里可以产生一个 signer 对象,可以和 以上的 client 直接发起交易。
产生随机账户,并打印地址。
#![allow(unused)] fn main() { let mut alice_account = LocalAccount::generate(&mut OsRng); info!("Alice account address is : {}", alice_account.address()); }
导出账户
通过 LocalAccount 的 private_key 方法,可以获取到私钥字节序列。然后使用 hex 编码打印导出。就可以供其他的钱包使用。
#![allow(unused)] fn main() { info!( "Alice account private key is : 0x{}", hex::encode(alice_account.private_key().to_bytes()) ); }
导入账户
通过 LocalAccount::from_private_key
的操作,可以导入账户。但是,导入账户的时候,无法知道用户的 sequence_number。
所以,需要从链上获取更新一次,供以后使用。
#![allow(unused)] fn main() { let client = Client::new(AptosBaseUrl::Testnet.to_url()); let mut import_account = LocalAccount::from_private_key( "0xb35ea8e82ec4daebab0892a466396bc5276e9d2fd05bbf9d4c5e3cacb0b90f68", 0, ) .unwrap(); let account_info = client.get_account(import_account.address()).await?; info!( "Import account sequence is : {}", account_info.inner().sequence_number ); import_account.set_sequence_number(account_info.inner().sequence_number); }
获取测试代币
devnet 和 testnet 都包含了水龙头的地址,可以获取测试代币。 使用水龙头的 Url 和 rest_client 构建一个 FaucetClient ,调用 fund 方法用来申请测试代币。
#![allow(unused)] fn main() { let faucet_url = url::Url::parse("https://faucet.testnet.aptoslabs.com")?; let faucet_client = FaucetClient::new_from_rest_client(faucet_url, client.clone()); let faucet_result = faucet_client .fund(import_account.address(), 100_000_000 * 5) .await?; info!("Faucet result: {:?}", faucet_result); }
获取账户余额
有几种方法获得账户的余额。
- 使用 client 直接获取
#![allow(unused)] fn main() { let balance = client.get_account_balance(account.address()).await?; info!("current balance is : {:?}", balance.inner().coin); }
- 使用 coin_client 获取
#![allow(unused)] fn main() { let coin_client = CoinClient::new(&client); let coin_balance = coin_client.get_account_balance(&account.address()).await?; info!("coin balance is : {:?}", coin_balance); }
- 更加通用的方式是使用 client.get_account_resource 方法获取。以上的两种方法其实也是使用这种方法获取的。
#![allow(unused)] fn main() { let another_resource = client .get_account_resource( account.address(), "0x1::coin::CoinStore<0xa8a5e68261a0f198e34deb2c0fd2683244e51dd015b8cb6987efefc61708d76a::Kana::Kana>", ) .await?; match another_resource.inner() { Some(resource) => { let coin = resource.data.get("coin").unwrap(); info!("another resource is : {:?}", coin.get("value")); } None => { info!("another resource is not found"); } } }
这种方法也适用于获取账户的资源对象解析。
发起转账
转账通过 coin_client 的 transfer 操作完成。其中通过 提供 TransferOptions 中的 coin_type 决定转移的 token 种类。 提供 None 表示转移默认的 Token apt。
#![allow(unused)] fn main() { let to_address = AccountAddress::from_str( &"0x1", ) .unwrap(); let mut transfer_option = TransferOptions::default(); transfer_option.coin_type = &"0xa8a5e68261a0f198e34deb2c0fd2683244e51dd015b8cb6987efefc61708d76a::Kana::Kana"; let tx = coin_client .transfer(&mut account, to_address, 1, Some(transfer_option)) .await?; info!("transfer tx : {:?}", tx.hash.to_string()); }
拿到交易hash ,并不表示交易完成。
等待交易完成
有时候需要等待交易完成再进行下一步的操作。 可以使用 client 的 wait_for_transaction 来完成。
#![allow(unused)] fn main() { client.wait_for_transaction(&tx).await?; }
调用过程中包含了一层模拟调用,并将结果的error 也会反馈在返回的结果中,
代码示例
以下是一个账户转账操作的完整实例。
#![allow(unused)] fn main() { use { anyhow::Result, aptos_sdk::{ coin_client::{CoinClient, TransferOptions}, rest_client::{AptosBaseUrl, Client}, types::{account_address::AccountAddress, LocalAccount}, }, log::info, std::str::FromStr, }; pub async fn runtime_job(_runtime: &crate::app::RunTime) -> Result<()> { let client = Client::new(AptosBaseUrl::Testnet.to_url()); let user_private_key = "0xb35ea8e82ec4daebab0892a466396bc5276e9d2fd05bbf9d4c5e3cacb0b90f68"; let mut account = LocalAccount::from_private_key(&user_private_key, 0)?; info!("current address : {}", account.address()); let account_data = client.get_account(account.address()).await?; let current_sequence = account_data.inner().sequence_number; account.set_sequence_number(current_sequence); let balance = client.get_account_balance(account.address()).await?; info!("current balance is : {:?}", balance.inner().coin); let coin_client = CoinClient::new(&client); let coin_balance = coin_client.get_account_balance(&account.address()).await?; info!("coin balance is : {:?}", coin_balance); let another_resource = client .get_account_resource( account.address(), "0x1::coin::CoinStore<0xa8a5e68261a0f198e34deb2c0fd2683244e51dd015b8cb6987efefc61708d76a::Kana::Kana>", ) .await?; match another_resource.inner() { Some(resource) => { let coin = resource.data.get("coin").unwrap(); info!("another resource is : {:?}", coin.get("value")); } None => { info!("another resource is not found"); } } let to_address = AccountAddress::from_str( &"0x1", ) .unwrap(); let mut transfer_option = TransferOptions::default(); transfer_option.coin_type = &"0xa8a5e68261a0f198e34deb2c0fd2683244e51dd015b8cb6987efefc61708d76a::Kana::Kana"; let tx = coin_client .transfer(&mut account, to_address, 1, Some(transfer_option)) .await?; info!("transfer tx : {:?}", tx.hash.to_string()); client.wait_for_transaction(&tx).await?; Ok(()) } }
First Dapp
我们将完成这样一个简单的dapp,并完成合约调用。 dapp 的逻辑关系是这样的。
- 用户(address)可以通过合约,mint 一个 Counter 对象。
- Counter 内部包含了一个数字 value ,一个字符串消息msg,同时锁定了0.1标准的代币。
- 用户(address) 可以调用合约修改Counter对象内部的value 的值。
- 用户(address) 可以销毁这个Counter对象,并取回其中锁定的代币。
交互过程中,也会产生对应的Event,我们也会通过rust 的rpc 调用来获取。
合约部分
module counter::counter {
use std::signer;
use std::coin;
use std::string::String;
use std::timestamp;
use std::event::emit;
const LOCK_COIN_VALUE:u64 = 10_000_000;
const EVENT_TYPE_CREATE:u8 = 0;
const EVENT_TYPE_INCREMENT:u8 = 1;
const EVENT_TYPE_DESTROY:u8 = 2;
#[event]
struct CounterEvent has drop,store {
sender: address,
value: u64,
timestamp: u64,
event_type: u8,
}
struct MyCounter<phantom CoinType> has key,store {
value: u64,
msg: String,
lock_coin: coin::Coin<CoinType>
}
fun new_counter_event(sender:address,value:u64,event_type:u8) : CounterEvent {
CounterEvent { sender, value, timestamp:timestamp::now_seconds(),event_type }
}
fun new_counter<CoinType>(value:u64,msg:String,sender:&signer) : MyCounter<CoinType> {
let lock_coin = coin::withdraw<CoinType>(sender, LOCK_COIN_VALUE);
emit(new_counter_event(signer::address_of(sender),value,EVENT_TYPE_CREATE));
MyCounter { value ,msg, lock_coin }
}
public entry fun mint<CoinType>(sender:signer,value:u64,msg: String) {
let sender_addr = signer::address_of(&sender);
if (!exists<MyCounter<CoinType>>(sender_addr)) {
move_to(&sender, new_counter<CoinType>(value,msg,&sender));
}
}
public entry fun increment<CoinType>(sender:signer,value:u64) acquires MyCounter {
let sender_addr = signer::address_of(&sender);
if (exists<MyCounter<CoinType>>(sender_addr)) {
let x = borrow_global_mut<MyCounter<CoinType>>(sender_addr);
x.value = x.value + value;
emit(new_counter_event(sender_addr,x.value,EVENT_TYPE_INCREMENT));
}
}
public entry fun destroy<CoinType>(sender:signer) acquires MyCounter{
let sender_addr = signer::address_of(&sender);
if (exists<MyCounter<CoinType>>(sender_addr)) {
emit(new_counter_event(sender_addr,0,EVENT_TYPE_DESTROY));
let MyCounter{value:_value,msg:_msg,lock_coin} = move_from<MyCounter<CoinType>>(sender_addr);
coin::deposit<CoinType>(sender_addr,lock_coin);
}
}
}
以上是这个dapp的智能合约,包含 一个泛型Struct MyCounter, 一个 Event Struct, 三个 entry 函数供外部操作。
部署
按照以下的步骤完成测试合约部署:
1. 初始化
aptos init
根据需要选择不同的网络 ,网络包括: devnet, testnet, mainnet, local, custom
。不同的网络账号间是不通用的。
下一步选择输入私钥或者生成一个新的。
最后,你将获得你的部署账号。把这个账户写入 合约配置文件的 address 模块。
[addresses]
counter = "0x9ce5950565b5cb8d514b09f5ae5afdd0ed75d41bcbb73409bd066378dcd4b7f3"
2. 编译:
aptos move compile --package-dir . --skip-fetch-latest-git-deps
3. 部署
aptos move publish --skip-fetch-latest-git-deps
部署完成后,接下来就可以通过 合约的地址来完成调用了。
合约调用
合约调用的逻辑大体上分为如下的几步:
- 获得
entry
调用入口,添加函数参数,泛型参数,构建 payload。 - 在
payload
的基础上,添加交易过期时间、链的Id 就可以完成一个未签名的交易内容。 - 通过
LocalAccount
的sign_with_transaction_builder
完成签名。 - 最后通过
rpc client
广播完成签名的交易,在链上执行。
entry 入口调用定义
一大部分的合约调用都属于合约里的entry
函数调用,通过一个账号地址定位到包的部署位置。
#![allow(unused)] fn main() { let package = AccountAddress::from_str(&package_addr)?; }
调用合约,之前需要调整好 Account
的 sequence_number
。
一个 package
地址中会发布很多的 module,所以需要调用合约entry 所在的 module 名称。通过ModuleId 来组织。
#![allow(unused)] fn main() { ModuleId::new(package, Identifier::new("counter").unwrap()), }
定义为好 package 中的模块,还需要 entry function 的名称。通过 Identifier 来定义
#![allow(unused)] fn main() { Identifier::new("mint").unwrap() }
参数组织
调用合约的参数分为两种,一种是泛型参数,一种是函数参数。两类参数都是提供一个数组传递。
- 泛型参数通过 TypeTag::from_str 就可以获取:
#![allow(unused)] fn main() { vec![TypeTag::from_str(&"0x1::aptos_coin::AptosCoin")?] }
- 正常的函数参数,需要通过 bcs 的序列化,转为字节序列
#![allow(unused)] fn main() { vec![ bcs::to_bytes(&(1024 as u64))?, bcs::to_bytes(&("hello world".to_string()))?, ], }
bcs::to_bytes 的参数,一定要制定参数的类型,这个要和链上的参数类型一致,否则合约将无法识别。
设置交易过期时间
这个获取当前系统的时间戳,即可。
#![allow(unused)] fn main() { let expire_at = time::SystemTime::now() .duration_since(time::UNIX_EPOCH)? .as_secs() + 30; }
小提示: 尽管大多数的服务器都配置NAT时间同步。但是,有时候,由于某些原因服务器的时间会出现延迟或者提前的情况。所以,需要把这个因素提前考虑进去。
签名交易
构建一个 TransactionBuilder
, 加入 payload
、expire_at
、 chain_id
三个参数。
把构建好的 builder 传递给 client完成签名。
#![allow(unused)] fn main() { let builder = TransactionBuilder::new(payload, expire_at, ChainId::new(chain_id)); let txn = account.sign_with_transaction_builder(builder); }
广播并等待交易
提交交易完成后,可以获取到交易的hash ,通过这个 hash 可以确认合约的执行状态。 wait_for_signed_transaction,将等待交易完成再执行下边的逻辑。
#![allow(unused)] fn main() { let signature = client.submit(&txn).await?; println!("Signature: {}", signature.inner().hash); client.wait_for_signed_transaction(&txn).await?; }
完整代码
一个完整的合约调用模块如下:
#![allow(unused)] fn main() { let package = AccountAddress::from_str(&package_addr)?; let txn = { let payload = TransactionPayload::EntryFunction(EntryFunction::new( ModuleId::new(package, Identifier::new("counter").unwrap()), Identifier::new("mint").unwrap(), vec![TypeTag::from_str(&"0x1::aptos_coin::AptosCoin")?], vec![ bcs::to_bytes(&(1024 as u64))?, bcs::to_bytes(&("hello world".to_string()))?, ], )); let expire_at = time::SystemTime::now() .duration_since(time::UNIX_EPOCH)? .as_secs() + 30; let builder = TransactionBuilder::new(payload, expire_at, ChainId::new(chain_id)); let txn = account.sign_with_transaction_builder(builder); txn }; let signature = client.submit(&txn).await?; println!("Signature: {}", signature.inner().hash); client.wait_for_signed_transaction(&txn).await?; }