跳转到帖子
View in the app

A better way to browse. Learn more.

WEB3论坛社区

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.

WEB3论坛

管理员
  • 注册日期

  • 上次访问

WEB3论坛 发布的所有帖子

  1. 课后练习附件中的工程是一个"Tic-Tac-Toe"游戏。 CopyX, select a space 1 | 2 | 3 -------------- 4 | 5 | 6 -------------- 7 | 8 | 9运行后是这样的一个棋盘。两个玩家依次落子。先排成“横”,“竖”,“斜”一条线的赢。 代码中其他文件忽略,只关注 game.rs/board.rs 这两个文件。里面有“TODO”提示。在提示的地方填充函数内容。 最后运行cargo test 提示测试通过: Copyrunning 27 tests test board::tests::a_space_can_only_be_taken_once ... ok test board::tests::finds_available_spaces_in_full_board ... ok test board::tests::o_plays_next ... ok test board::tests::finds_available_spaces_in_empty_board ... ok test board::tests::a_space_above_the_board_cant_be_chosen ... ok test board::tests::a_negative_space_cant_be_chosen ... ok test board::tests::finds_available_spaces_in_an_in_progress_board ... ok test board::tests::starts_with_no_moves ... ok test board::tests::x_plays_first ... ok test board::tests::takes_a_number_of_rows ... ok test game::tests::a_tied_game_is_tied ... ok test game::tests::a_won_game_is_not_tied ... ok test game::tests::a_won_game_with_a_full_board_is_not_tied ... ok test game::tests::an_empty_game_is_not_tied ... ok test game::tests::an_empty_game_is_not_won ... ok test game::tests::check_if_game_won_by_x_is_won ... ok test game::tests::check_if_game_won_by_o_is_won ... ok test game::tests::check_line_won_by_x ... ok test game::tests::check_if_tied_game_is_won ... ok test game::tests::check_row_not_won_by_o ... ok test game::tests::find_winner_when_nobody_has_won ... ok test game::tests::find_winner_when_o_has_won ... ok test game::tests::find_winner_when_x_has_won ... ok test game::tests::game_is_over_when_board_is_full ... ok test game::tests::o_is_current_player_after_one_move ... ok test game::tests::game_not_over_when_board_is_empty ... ok test game::tests::x_is_current_player_at_start_of_game ... ok test result: ok. 27 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00scargo run可以正常游戏 参考答案board.rs Copypub fn get_size(&self) -> &i32 { // TODO: 返回size &self.size } pub fn get_spaces(&self) -> &Vec<i32> { // TODO: 返回spaces &self.spaces } pub fn is_space_available(&self, space: &i32) -> bool { // TODO: 调用contains判断 space是否在切片spacees内 !self.spaces.contains(space) } fn is_space_in_bounds(&self, space: &i32) -> bool { // TODO: 判断space位置是否在棋盘size*size返回内 let max_space = self.size * self.size; let min_space = 0; space >= &min_space && space < &max_space } pub fn get_available_spaces(&self) -> Vec<i32> { // TODO: 判断棋盘上是否有位置is_space_available // 提示,遍历位置,调用is_space_available方法 let all_spaces = 0..self.size * self.size; all_spaces .filter(|space| self.is_space_available(space)) .collect() }game.rs Copypub fn find_current_player(board: &Board) -> Marker { // TODO: 根据棋盘当前的棋子,判断下个棋子,是X还是O if board.get_spaces().len() % 2 == 0 { Marker::X } else { Marker::O } } pub fn find_winner(board: &Board) -> Marker { // TODO: 调用is_game_won_by 判断是 X赢了还是O赢了 if is_game_won_by(board, &Marker::X) { Marker::X } else if is_game_won_by(board, &Marker::O) { Marker::O } else { Marker::NA } }练习工程 参考答案
  2. Rustaceans 的理解如果没有其他语言为基础。那么不建议首先学习 Rust。但是有了其他语言的基础,又会对 Rust 的语言中的一些不那么常见的语法所难倒。 这里 Rustaceans 是对 Rust 程序员的一种昵称,我们在写代码的时候,也需要尽量的用 Rust 的思维来写,而不是对其他语言的翻译。比如将已有的 Solidity 翻译成 Rust。 这里标题为 Rustaceans,其实内容是 Rust 的陷阱与缺陷。主要讲一些 Rust 里面比较难理解的语法。 内存管理rust 不是说不需要像 C++一样 new/delete,自己开辟/释放内存么?怎么还需要说内存管理。 所有权和借用智能指针rust 的智能指针,主要提供了 Box<T> 在堆上分配空间 Rc<T> 引用计数,可以使得一个对象有多个 owner Ref<T> and RefMut<T>, RefCell<T> 强制要求在运行时检查借用关系,而不是编译期间,就有点动态检查的意思 Box<T>box 顾名思义,就是装箱,在 Objective-C 中有相关概念。本质就类似 C 里面 alloc 一段内存,然后将值 copy 过去。 Copyfn main() { let b = Box::new(5); println!("b = {}", b); }这个时候,b 实际上存储的是一个指针,指向一段放了数字 5 的内存,这段内存在堆上面。 类似这样的定义: Copyenum List { Cons(i32, Box<List>), Nil, } use crate::List::{Cons, Nil}; fn main() { let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil)))))); }就好比是 C++里面的前向类声明,然后存一个该类的指针。如果这里不用 Box,就会导致,这里在推测使用了多少空间的时候,陷入了循环。而 Box 只需要放一个指针大小就可以了。具体的内容在里面指向。 Rc<T>Rc:Reference Count,也就是 C++里面智能指针最常见的方式,当某个空间需要使用时,就对其计数加一。当不需要的时候,就减一。当引用技术的值为 0 的时候,就对其进行销毁。 比如这样的代码: Copyenum List { Cons(i32, Box<List>), Nil, } use crate::List::{Cons, Nil}; fn main() { let a = Cons(5, Box::new(Cons(10, Box::new(Nil)))); let b = Cons(3, Box::new(a)); let c = Cons(4, Box::new(a)); }会出错: Copyerror[E0382]: use of moved value: `a` --> src/main.rs:11:30 | 9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil)))); | - move occurs because `a` has type `List`, which does not implement the `Copy` trait 10 | let b = Cons(3, Box::new(a)); | - value moved here 11 | let c = Cons(4, Box::new(a)); | ^ value used here after move因为这里在用 Box 创建 b 的时候,已经将 a 借用了。接着又在创建 c 的时候,借用了 a,此时 a 所表达的空间的 owner 已经不再是 a。因此报错。 这里可以修改成: Copyenum List { Cons(i32, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::rc::Rc; fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); let b = Cons(3, Rc::clone(&a)); let c = Cons(4, Rc::clone(&a)); }首先将 a 定义为 Rc,是一个引用计数智能指针,它包含了空间内容,和空间计数。 每次 Rc::clone 的时候,都会将计数器+1,同时返回一个 Rc,其中内容指向的是同一个地方,但是引用计数+1。 因此就可以同时创建 b 和 c 了。 RefCell<T>前面借用有介绍到,不可以在 mut 借用后,继续可读借用。 比如代码: Copyfn main() { let x =String::from("hello, world"); let y = x.borrow_mut(); let z = x.borrow(); print!("y:{}, z:{}", y,z); }报错: Copy--> src/main.rs:1:5 | 1 | use std::cell::RefCell; | ^^^^^^^^^^^^^^^^^^ | = note: `#[warn(unused_imports)]` on by default error[E0599]: no method named `borrow_mut` found for struct `String` in the current scope --> src/main.rs:6:15 | 6 | let y = x.borrow_mut(); | ^^^^^^^^^^ method not found in `String` | ::: /Users/changzeng/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/src/rust/library/core/src/borrow.rs:206:8 | 206 | fn borrow_mut(&mut self) -> &mut Borrowed; | ---------- the method is available for `String` here | = help: items from traits can only be used if the trait is in scope help: the following trait is implemented but not in scope; perhaps add a `use` for it: | 1 + use std::borrow::BorrowMut; | error[E0599]: no method named `borrow` found for struct `String` in the current scope --> src/main.rs:7:15 | 7 | let z = x.borrow(); | ^^^^^^ method not found in `String` | ::: /Users/changzeng/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/src/rust/library/core/src/borrow.rs:179:8 | 179 | fn borrow(&self) -> &Borrowed; | ------ the method is available for `String` here | = help: items from traits can only be used if the trait is in scope help: the following trait is implemented but not in scope; perhaps add a `use` for it: | 1 + use std::borrow::Borrow; |现在修改成 RefCell: Copyuse std::cell::RefCell; fn main() { let x = RefCell::new(String::from("hello, world")); let y = x.borrow_mut(); let z = x.borrow(); print!("y:{}, z:{}", y,z); }虽然不可以运行,但是却可以通过编译。因为在运行的时候,还是会检查借用关系: CopyRunning `target/debug/helloworld` thread 'main' panicked at 'already mutably borrowed: BorrowError', src/main.rs:7:15 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace那这个有啥用呢?还不如在编译期间检查。那比如如下代码呢? Copylet x = RefCell::new(String::from("hello, world")); if your_x_switch { let y = x.borrow_mut(); } if (your_z_switch) { let z = x.borrow(); }仅通过编译是没法区分 if 分支的,但是再运行时,可以保证只走一个分支。 生命周期首先生命周期修饰是一个泛型修饰,也就是意味他是针对类型的。生命周期主要用来解决悬垂指针问题。也就是引用了一个已经被释放的空间。 那么如何保证被引用的空间一定没有被释放呢?就需要通过生命周期修饰,使得 rust 知道某个空间还在被引用中,不可以自动释放。 比如: Copyfn main() { let r; // ---------+-- 'a // | { // | let x = 5; // -+-- 'b | r = &x; // | | } // -+ | // | println!("r: {}", r); // | } // ---------+这里,编译会报错: Copy--> src/main.rs:6:13 | 6 | r = &x; | ^^ borrowed value does not live long enough 7 | } | - `x` dropped here while still borrowed 8 | 9 | println!("r: {}", r); | - borrow later used here在上面的代码部分,已经用注释吧生命周期范围罗列出来了,因为 r 借用超过了'b 的空间,所以报错,因为超过'b 后,x 不再存在。 所以一般我们在描述生命周期的是,也采用'a 'b 的形式。 再来看一个编译错误的例子: Copyfn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y } } fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {}", result); }报错为: Copy--> src/main.rs:9:33 | 9 | fn longest(x: &str, y: &str) -> &str { | ---- ---- ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y` help: consider introducing a named lifetime parameter | 9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { | ++++ ++ ++ ++修改方式,编译器也给出来了: Copyfn longest<'a>(x: &'a str, y: &'a str) -> &'a str这里<'a>是一个泛型修饰符,在原有的&str 中间增加 'a 来修饰这个参数的生命周期。 上面的修改,要求传入的参数生命周期,x、y、z 三者的最小范围要一致。 错误处理在类似 java/c++中,对错误处理,有抛异常的方式, 而类似 go 这样的,一般会通过多返回值的方式,返回一个错误。而在 rust 中,一般通过 Enum 的形式,返回一直结果返回值。 首先来看这里说的结果返回值,Result 的定义。这个定义是标准库中,可以在不用做特别 use 的情况下直接使用: Copyenum Result<T, E> { Ok(T), Err(E), }本质上他就是个枚举。包含两种可能,要么是一个 T,要么是一个 E。T 就是成功时应该正确返回的值类型,而 E 就是错误时,返回的错误类型。 来看一段使用案例: Copyuse std::fs::File; fn main() { let greeting_file_result = File::open("hello.txt"); let greeting_file = match greeting_file_result { Ok(file) => file, Err(error) => panic!("Problem opening the file: {:?}", error), }; }因为 Resutl 是个 enum,因此,这里可以通过 match 语法,来对其进行类型匹配。 open 的原型为: Copypub fn open<P: AsRef<Path>>(path: P) -> io::Result<File>这里 io::Result 定义为: Copypub type Result<T> = result::Result<T, Error>;而 Error 是 std::io::error 定义的: Copypub struct Error { repr: Repr, }所以 Rust 可以推导出如果文件打开失败,这里 greeting_file_result,返回的是 std::io::error::Error,并放在枚举 Err(error)中包裹。通过 match 可以解出来进行打印。 测试代码测试代码主要分为单元测试和集成测试。类似 go 语言特性,go 语言在标准库和工具层面提供了单元测试的方法。 rust 也在工具和标准库层面提供了类似地方方法。除此之外,rust 还提供了集成测试框架模板。 单元测试在代码所在文件中,添加一个 test 的 mod 并用#[cfg(test)] 特性控制,然后在要执行 test 的函数上增加修饰 [test] 即可: Copy#[cfg(test)] mod tests { #[test] fn it_works() { let result = 2 + 2; assert_eq!(result, 4); } }这样当我们执行 Copycargo test Compiling testcase v0.1.0 (Solana-Asia-Summer-2023/s101/Solana-Rust/demo/testcase) Finished test [unoptimized + debuginfo] target(s) in 0.23s Running unittests src/lib.rs (target/debug/deps/testcase-4146fa835bb26be8) running 1 test test tests::test_fun ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests testcase running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s就会执行 test mod 里面的 it_works 函数了。 在上面的测试代码中,我们用 assert_eq 宏判断了是否相等,如果失败的话,测试结果就是失败。我们还可以用 panic强行失败,这里我们增加测试代码: Copy#[test] fn test_panic() { panic!("got panic"); }失败 的时候表现为: Copycargo test Compiling testcase v0.1.0 (Solana-Asia-Summer-2023/s101/Solana-Rust/demo/testcase) Finished test [unoptimized + debuginfo] target(s) in 0.30s Running unittests src/lib.rs (target/debug/deps/testcase-4146fa835bb26be8) running 2 tests test tests::test_fun ... ok test tests::test_panic ... FAILED failures: ---- tests::test_panic stdout ---- thread 'tests::test_panic' panicked at 'got panic', src/lib.rs:12:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::test_panic test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib`集成测试如果某个测试,需要涉及到多个模块的代码,并且还有一些初始化或者条件的设置。 我们可以在工程目录下新增一个 "tests" 目录,然后在在 tests 目录下增加文件或者目录。 在 tests 目录下的单个文件中,此时可以不用使用 test 模块。直接写要测试的逻辑。只需要在要测试的逻辑函数的上面用 #[tset]进行修饰即可。 比如: Copy. ├── Cargo.lock ├── Cargo.toml ├── src │ └── lib.rs └── tests ├── test01.rs └── test_files ├── func.rs └── main.rs指定执行测试: Copycargo test --test test01 Finished test [unoptimized + debuginfo] target(s) in 0.00s Running tests/test01.rs (target/debug/deps/test01-0c980e86b9bfdada) running 1 test test test_main ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
  3. 通过Cargo管理工程cargo作为rust的工程管理工具。类似go语言的gomod。其主要通过 Cargo.toml作为配置文件, 配合cargo 二进制工具来管理依赖。 cargo在通过rustup安装rust的过程中已经安装好了。Cargo.toml在通过cargo new 创建工程的时候,自动生成。 在构建的时候,cargo 会根据Cargo.toml中的依赖,自动拉取依赖的代码,并将相应的版本信息,资源签名,记录在 Cargo.lock文件中,该文件类似go.sum文件,记录了各个依赖的meta信息。 cargo命令创建工程首先通过cargo命令,可以创建工程。创建的工程分成两类,一类是库,一类是二进制可执行程序。 通过cargo new project_name 命令可以创建project_name的工程。默认工程是一个可执行程序。 通过指定--lib可以指定其为库项目。一个工程,只能包含一个库目标,但是可以包含多个二进制程序。 添加依赖当需要依赖外部库的时候,首先要将其加入到工程中: Copycargo add [options] crate… cargo add [options] --path path cargo add [options] --git url [crate…]三种不同的参数,可以针对三种情况的依赖。 直接跟库名,会去cargo.io上索引,找到最新的版本 --path指定库在本地的路径,可以对本地目录进行依赖 --git 则指定的git仓库的路径,比如是私有的git仓库 通过 cargo remove 可以移除相关的依赖。 构建 & 执行前面已经接触了构建。直接用build就可以了: Copycargo build [options]这里有几个参数 --workspace: 构建整个workspace里面的目标 --lib: 构建库目标 --bin name…: 只构建指定的可执行文件 --example name…: 只构建指定的example --test name…: 构建指定的test --release: 采用relase构建 而通过: Copycargo clean [options]则可以清除构建结果 执行通过run命令来发起: Copycargo run [options] [-- args]其中如果是传递给cargo的flag直接传入。如果要传递给被执行的程序。则需要使用 "--" 做分割。其后的 flag才是传递给要运行的程序的。 --bin name…: 只执行指定的可执行文件 --example name…: 只执行指定的example 比如 Copycargo run --bin helloworld Finished dev [unoptimized + debuginfo] target(s) in 0.04s Running `target/debug/helloworld` Please use ./hellowolrd name.没有携带参数。 如果是这样: Copycargo run --bin helloworld -l error: unexpected argument '-l' found tip: to pass '-l' as a value, use '-- -l' Usage: cargo run [OPTIONS] [args]... For more information, try '--help'.这里实际上是把-l传递给了cargo run ,但是cargo run本身是不接受"-l"的flag的。所以这里报错了。 Copycargo run --bin helloworld -- -l Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/helloworld -l` Hello -l通过"--"的分割,我们将flag参数跳过cargo run传递给可执行程序。 更多其他参数可以参考 The Cargo Book Cargo.toml结构每个Cargo.toml包含如下内容: [cargo-features] --- Unstable, nightly-only features. [[package]] --- Defines a package. [name] --- The name of the package. [version] --- The version of the package. [authors] --- The authors of the package. [edition] --- The Rust edition. [rust-version] --- The minimal supported Rust version. [description] --- A description of the package. [documentation] --- URL of the package documentation. [readme] --- Path to the package's README file. [homepage] --- URL of the package homepage. [repository] --- URL of the package source repository. [license] --- The package license. [license-file] --- Path to the text of the license. [keywords] --- Keywords for the package. [categories] --- Categories of the package. [workspace] --- Path to the workspace for the package. [build] --- Path to the package build script. [links] --- Name of the native library the package links with. [exclude] --- Files to exclude when publishing. [include] --- Files to include when publishing. [publish] --- Can be used to prevent publishing the package. [metadata] --- Extra settings for external tools. [default-run] --- The default binary to run by [cargo run]. [autobins] --- Disables binary auto discovery. [autoexamples] --- Disables example auto discovery. [autotests] --- Disables test auto discovery. [autobenches] --- Disables bench auto discovery. [resolver] --- Sets the dependency resolver to use. Target tables: [[lib]] --- Library target settings. [[[bin]]] --- Binary target settings. [[[example]]] --- Example target settings. [[[test]]] --- Test target settings. [[[bench]]] --- Benchmark target settings. Dependency tables: [[dependencies]] --- Package library dependencies. [[dev-dependencies]] --- Dependencies for examples, tests, and benchmarks. [[build-dependencies]] --- Dependencies for build scripts. [[target]] --- Platform-specific dependencies. [[badges]] --- Badges to display on a registry. [[features]] --- Conditional compilation features. [[patch]] --- Override dependencies. [[replace]] --- Override dependencies . [[profile]] --- Compiler settings and optimizations. [[workspace]] --- The workspace definition. 整个的完整的内容会比较多,普通情况下只需要使用默认生成的文件,然后在里面填充dependence即可。 作为实践,一般将Cargo.toml分成两类。对于一个大repo,会将所有的代码放在一个目录下面,通过一个包含workspace 的Cargo.toml来管理其他自Cargo.toml。类似Makefile的嵌套管理。 比如solana工程的: Copy[workspace] members = [ "account-decoder", "accounts-bench", ... ] exclude = [ "programs/sbf", ] # This prevents a Travis CI error when building for Windows. resolver = "2" [workspace.package] version = "1.17.0" authors = ["Solana Labs Maintainers <maintainers@solanalabs.com>"] repository = "https://github.com/solana-labs/solana" homepage = "https://solanalabs.com/" license = "Apache-2.0" edition = "2021" [workspace.dependencies] aes-gcm-siv = "0.10.3" ahash = "0.8.3" ...这里能看到,主要结构就是通过workspace.members来指定了子目录。 exclude指定不要的目录。workspace.dependencies指定了整个工作 区要依赖的库。 另外一种就是具体的执行程序或者库的目录,也就是workspace管理的具体子目录,这里比如solana cli的目录: Copy[package] name = "solana-cli" description = "Blockchain, Rebuilt for Scale" documentation = "https://docs.rs/solana-cli" version = { workspace = true } authors = { workspace = true } repository = { workspace = true } homepage = { workspace = true } license = { workspace = true } edition = { workspace = true } [dependencies] bincode = { workspace = true } bs58 = { workspace = true } ... [dev-dependencies] solana-streamer = { workspace = true } solana-test-validator = { workspace = true } tempfile = { workspace = true } [[bin]] name = "solana" path = "src/main.rs" [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"]这里package下面的键指定了库的属性,比如名字,描述。而dependencies指定了依赖, 其中 { workspace = true } 表示其继承workspace父目录中的 相关位置版本的信息。 最后这通过[[bin]]定义了这里有个可执行程序叫: "solana" 工程目录结构我们来看一个相对复杂的cargo工程目录: Copy├── Cargo.lock ├── Cargo.toml ├── examples │ ├── example01.rs │ └── example_files │ ├── func.rs │ └── main.rs ├── src │ ├── bin │ │ ├── bin1.rs │ │ └── bin2.rs │ └── lib.rs └── tests ├── test01.rs └── test_files ├── func.rs └── main.rs在这个demo里面,我们主要包含了 三个目录: src: 库和二进制文件 example: 例子 tests: 集成测试 可执行程序可执行程序,可以将其放入src/bin目录下。每个文件可以有自己单独的main函数。比如这里: Copybin1.rs: use cargodir::lib_func; fn main() { lib_func(); println!("it is bin1"); }使用到的库函数在lib.rs中定义: Copypub fn lib_func() { println!("lib_func"); }但是在可执行程序文件中,通过use来包含,然后在main函数中调用。 如果不是按照src/bin目录来组织代码的,需要在Cargo.toml中进行指定,比如用cli目录: Copy[[bin]] name = "bin1" path = "src/cli/bin1.rs" [[bin]] name = "bin2" path = "src/cli/bin2.rs"这样就可以通过--bin来指定要执行哪个name的可执行程序了: Copycargo run --bin bin1 Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/bin1` lib_func it is bin1例子程序示例程序可以通examples目录来管理。其中可以是单个文件,也可以用一个目录来组织多个文件。单个文件和目录中都可以实现 main函数作为示例程序的入口: 比如example01.rs: Copyfn main() { println!("it is example 01 "); }这样只要执行: Copycargo run --example example01 Compiling cargodir v0.1.0 (Solana-Asia-Summer-2023/s101/Solana-Rust/demo/cargodir) Finished dev [unoptimized + debuginfo] target(s) in 0.20s Running `target/debug/examples/example01` it is example 01集成测试程序单元测试是放在实现文件中的,如果有集成测试,则可以类似例子一样,组织在tests目录中。一样可以单个文件或者多个文件放在一个目录中。 比如:test01.rs: Copy#[test] fn test_main() { println!("it is test 01"); }运行: Copycargo test --test test01 Compiling cargodir v0.1.0 (Solana-Asia-Summer-2023/s101/Solana-Rust/demo/cargodir) Finished test [unoptimized + debuginfo] target(s) in 0.22s Running tests/test01.rs (target/debug/deps/test01-de791c18df3f4346) running 1 test test test_main ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s参考The Cargo Book
  4. Rust基本语法变量和类型基本数据类型基本数据类型主要有整形、浮点、布尔以及字符类型。 整形按照所占空间大小被分为1、2、4、8、16 字节大小的整数。每个大小又有有符号和无符号的差别。 具体的定义如下: Length Signed Unsigned 8-bit i8 u8 16-bit i16 u16 32-bit i32 u32 64-bit i64 u64 128-bit i128 u128 arch isize usize 而浮点型包括f32和f64两个分别使用4字节和8字节的IEEE-754 浮点格式的浮点数。 布尔类型和其他语言的布尔类型类似,用true和false来表示。 字符类型是用''单引号括起来的字符。rust天生支持utf-8,所以任何单引号括起来的utf-8字符都是合法的字符类型变量。 复合类型复合类型是基本类型和复合类型的组合,典型的有 元组: Copylet tup: (i32, f64, u8) = (500, 6.4, 1);单纯的把几个类型放在一起。访问的时候通过下标索引来访问 比如 这里的500:tup.0 数组: Copylet arr = [1, 2, 3, 4, 5];和元组不通的是,这里每个元素的类型必须是同样的。访问的时候,下标用中括号表示: arr[0] Copystruct: struct User { active: bool, username: String, email: String, sign_in_count: u64, } let user = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, };struct类似C语言里的struct,将多个类型组合在一起,通过成员名进行访问:user.email 变量变量定义为: Copylet x = 5;这里x的类型是由系统推到而来的。也可以显示指定类型 Copylet x:u32 = 5;这里x被赋值为5后,区别与其他语言的点。变量默认是不可以修改的,也就是: Copylet x = 5; x=6;会导致报错: Copyerror[E0384]: cannot assign twice to immutable variable `x` --> src/main.rs:4:5 | 2 | let x = 5; | - | | | first assignment to `x` | help: consider making this binding mutable: `mut x` 3 | println!("The value of x is: {x}"); 4 | x = 6; | ^^^^^ cannot assign twice to immutable variable但是可以用如下形式: Copylet x = 5; let x=6;甚至如下形式: Copylet x = 5; let x="6";这里,第二个let相当于重新定义一个变量,可以重新定义其类型。 如果要修改变量,可以这样定义: Copylet mut x = 5; x=5;语句和表达式语句是指执行一段逻辑动作的代码,比如if语句,while语句。而表达式,是可以得到结果值的代码。比如1+1。 虽然表达式也可以执行逻辑,但是区别是表达式可以作为返回值,或者别的变量的赋值。而语句不行。 let表达式let 主要用于变量的定义: Copylet condition = true;let还可以和if组合: Copylet number = if condition { 5 } else { "six" };这里if语句里面的值类似返回值。 match表达式match可以做类型匹配和解包: Copylet config_max = Some(3u8); match config_max { Some(max) => println!("The maximum is configured to be {}", max), _ => (), }等同于上面的if let: Copylet config_max = Some(3u8); if let Some(max) = config_max { println!("The maximum is configured to be {}", max); }条件 语句if和Go语言的if比较类似,都是去除了传统语言里面的括号 Copylet number = 3; if number < 5 { println!("only if"); } if number < 5 { println!("if else: condition was true"); } else { println!("if else: condition was false"); } if number < 5 { println!("if else if : number < 5 "); } else if number == 5 { println!("if else if : number == 5 "); } else{ println!("if else if : number > 5 "); }循环语句rust给无限循环增加了一个loop,约等于 while 1: Copyloop { println!("again and again!"); }但是loop可以通过break来返回一个结果: Copylet mut counter = 0; let result = loop { counter += 1; if counter == 10 { break counter * 2; } }; println!("The result is {result}");在循环中,可以通过类似goto的label定义,break到对应层级: Copylet mut count = 0; 'counting_up: loop { println!("count = {count}"); let mut remaining = 10; loop { println!("remaining = {remaining}"); if remaining == 9 { break; } if count == 2 { break 'counting_up; } remaining -= 1; } count += 1; } println!("End count = {count}");除了loop,常见的while也是有的: Copywhile true { println!("again and again!"); }以及for语句,不同于c-like语言,for语句是迭代器风格的,而不是两个";"三语句模式。 Copylet a = [10, 20, 30, 40, 50]; for element in a { println!("the value is: {element}"); } // 模拟 int i=1; i<4; i++ for number in (1..4).rev() { println!("{number}!"); }函数函数分为main函数和普通函数。main函数是可执行程序的入口函数。对于库是不需要的。main函数本身也是个普通函数 只是函数名为main。 Copyfn main() { println!("Hello, world!"); another_function(); } fn another_function() { println!("Another function."); }上面这个是不带参数,没有返回值的函数的最基本定义结构。首先用 fn开始,然后跟函数名以及()。最后用{},括起来 的函数逻辑. Copyfn main() { print_labeled_measurement(5, 'h'); } fn print_labeled_measurement(value: i32, unit_label: char) { println!("The measurement is: {value}{unit_label}"); }带参数的函数,在()中定义参数,参数为参数名:类型这样的格式。 带返回的函数如: Copyfn main() { let x = plus_one(5); println!("The value of x is: {x}"); } fn plus_one(x: i32) -> i32 { x + 1 }在参数()和函数体{}中间用-> 分割,罗列返回值类型。 这里体现了语句和表达式的区别。语句是执行一个动作,不具有可以作为直的结果。而表达式是可以作为值的结果的。 因此这里表达式x+1,作为值,直接进行范围。注意,此时后面不可以加;。 struct和enumstruct定义结构体如下: Copystruct User { active: bool, username: String, email: String, sign_in_count: u64, }结构为struct关键字加上大写开头的类型名,后面跟{}包裹的成员变量,每个成员变量为成员名:类型,。 struct的初始化: Copylet user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, };如果用其他struct来初始化同类型时,可以用: Copylet user2 = User { email: String::from("another@example.com"), ..user1 };有一种特殊的struct,元组struct: Copystruct Color(i32, i32, i32); struct Point(i32, i32, i32); fn main() { let black = Color(0, 0, 0); let origin = Point(0, 0, 0); println!("black 0:{}", black.0) }还有种更特殊的struct: Copystruct AlwaysEqual; fn main() { let subject = AlwaysEqual; }他的值可以是{},所以在代码中看到{}就可以认为是一个不占空间的struct的值,比如: CopyOk({})enumrust中的enum比任何其他语言的都强大。 简单版本: Copyenum IpAddrKind { V4, V6, }这个容易理解,使用的时候就是 let four = IpAddrKind::V4;。 指定类型版本: Copyenum IpAddr { V4(String), V6(String), }使用的时候:let home = IpAddr::V4(String::from("127.0.0.1")); 这就有点不像传统enum了。更像一个struct定义。 这里"V4","V6"同样的类型,还不直观。 Copystruct Ipv4Addr { // --snip-- } struct Ipv6Addr { // --snip-- } enum IpAddr { V4(Ipv4Addr), V6(Ipv6Addr), }V4是一个类型,V6是一个类型。这个时候enum更像union。也还好理解。 来个不容易看的: Copyenum IpAddr { V4(u8, u8, u8, u8), V6(String), } let home = IpAddr::V4(127, 0, 0, 1);这里V4变成了一个元组类型。虽然可读性差一点,但是写法方便。 最复杂的: Copyenum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), }Message定义了一个游戏的消息指令,这里在传统语言如C++中,可能要定义个Message基类,然后每个命令消息再去定义子类。 而这里一个enum搞定。这个时候,这个enum真不好说他是什么功能。 给struct/enum定义方法给struct定义方法: Copy#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!( "The area of the rectangle is {} square pixels.", rect1.area() ); }使用impl Xxx {} 语法,在{}的方法,就是成员方法。 其中不带(&self)参数的是类方法,第一个参数为(&self)的为类方法。这个类似python2。 给enum定义也是一样,用impl Xxx {} Copyimpl Message { fn call(&self) { // method body would be defined here } } let m = Message::Write(String::from("hello")); m.call();枚举的强大还体现在他的使用上,再不需要上面的类继承来做反射: Copypub enum NameRegistryInstruction { Create { hashed_name: Vec<u8>, lamports: u64, space: u32, }, Update { offset: u32, data: Vec<u8> }, Transfer { new_owner: Pubkey }, Delete, Realloc { space: u32, }, } match instruction { NameRegistryInstruction::Create { hashed_name, lamports, space, } => { msg!("Instruction: Create"); Processor::process_create(program_id, accounts, hashed_name, lamports, space)?; } NameRegistryInstruction::Update { offset, data } => { msg!("Instruction: Update Data"); Processor::process_update(accounts, offset, data)?; } NameRegistryInstruction::Transfer { new_owner } => { msg!("Instruction: Transfer Ownership"); Processor::process_transfer(accounts, new_owner)?; } NameRegistryInstruction::Delete => { msg!("Instruction: Delete Name"); Processor::process_delete(accounts)?; } NameRegistryInstruction::Realloc { space } => { msg!("Instruction: Realloc Name Record"); Processor::process_realloc(accounts, space)?; } }通过match语句,首先能对enum做类型匹配,匹配的同时,还可以对类型做解包, 如NameRegistryInstruction::Create Copy{ hashed_name, lamports, space, }这里其实是一个没有给名字的struct。 容器list/vectorCopyVec<T>vec是std提供的链表类型。可以用来存放相同类型的数组。 创建指定类型的vector: Copylet v: Vec<i32> = Vec::new();也可以通过vec!宏直接赋值初始化: Copylet v = vec![1, 2, 3];增加元素: Copylet mut v = Vec::new(); v.push(5); v.push(6); v.push(7); v.push(8);这里mut表示vector是可以做增删修改的。 通过remove进行删除元素: Copylet mut v = vec![1, 2, 3]; assert_eq!(v.remove(1), 2); assert_eq!(v, [1, 3]);可以通过get给定下标,获取vector的元素,通过Option来做判断是否存在: Copylet v = vec![1, 2, 3, 4, 5]; let third: &i32 = &v[2]; println!("The third element is {third}"); let third: Option<&i32> = v.get(2); match third { Some(third) => println!("The third element is {third}"), None => println!("There is no third element."), }通过迭代器,遍历: Copylet v = vec![100, 32, 57]; for n_ref in &v { // n_ref has type &i32 let n_plus_one: i32 = *n_ref + 1; println!("{n_plus_one}"); }上面这个是只读的,如果需要修改,使用: Copylet mut v = vec![100, 32, 57]; for n_ref in &mut v { // n_ref has type &mut i32 *n_ref += 50; }Stringrust的String不是基础类型,是由std提供的类型。创建字符串可以用: Copylet mut s = String::new(); let data = "initial contents"; let s = data.to_string(); let s = String::from("initial contents");三种方法。 修改字符串: Copylet mut s = String::from("foo"); s.push_str("bar"); let mut s1 = String::from("foo"); let s2 = "bar"; s1.push_str(s2); println!("s2 is {s2}"); let mut s = String::from("lo"); s.push('l'); let s1 = String::from("Hello, "); let s2 = String::from("world!"); let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used可以使用push_str、push甚至是"+"来修改字符串。注意"+"会borrow变量。当然要修改字符串前提是字符串是"mut"的。 可以通过下标操作。或者字符串中的字符: Copylet s1 = String::from("hello"); let h = s1[0];Maprust中的map也不是基础类型,而是std提供的,并且也和一般语言中不一样,类似Java里面明确的命名为"HashMap" 创建: Copyuse std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50);访问: Copyuse std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); let team_name = String::from("Blue"); let score = scores.get(&team_name).copied().unwrap_or(0);遍历: Copyuse std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); for (key, value) in &scores { println!("{key}: {value}"); }插入修改: Copyuse std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Blue"), 25); println!("{:?}", scores);判断是否存在,存在才插入: Copyuse std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.entry(String::from("Yellow")).or_insert(50); scores.entry(String::from("Blue")).or_insert(50); println!("{:?}", scores);traittrait 类似其他语言中的接口,但是就好比enum一样,他不仅类似其他语言的接口,他更强大。 更复杂。很多继承的特性都是通过 trait 来实现。 trait 的定义类似struct: Copypub trait Summary { fn summarize(&self) -> String; }trait主要是定义方法。这里方法就是个普通的函数定义。但是去掉了参数名。 trait只是定义了接口方法,而具体实现需要再struct中实现: Copypub struct NewsArticle { pub headline: String, pub location: String, pub author: String, pub content: String, } impl Summary for NewsArticle { fn summarize(&self) -> String { format!("{}, by {} ({})", self.headline, self.author, self.location) } } pub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool, } impl Summary for Tweet { fn summarize(&self) -> String { format!("{}: {}", self.username, self.content) } }使用impl关键字,跟要实现的trait名加上"for Xxx"给具体的struct视线。 这里还可以再trait中给出默认实现: Copypub trait Summary { fn summarize(&self) -> String { String::from("(Read more...)") } }这样,在"impl"里面没有"summarize"方法实现时,就默认用trait里面的定义。 trait的作用主要要结合泛型限制,和参数专递才能体现。比如类似其他语言中基本的OOP的动态: Copypub fn notify(item: &impl Summary) { println!("Breaking news! {}", item.summarize()); }这里将参数声明为trait,任何实现了这个traint的类型都可以传递进来。具体的summarize是 传入进来的类型的实现。 除了可以限制参数,还可以限制返回值。这个就有点类似其他语言的基类、接口。 Copyfn returns_summarizable() -> impl Summary { Tweet { username: String::from("horse_ebooks"), content: String::from( "of course, as you probably already know, people", ), reply: false, retweet: false, } }和泛型的结合,见下一章。 泛型泛型是rust最强的地方,也是rust最难的地方,更是学习rust的拦路虎。 来看个定义: Copypub struct Iter<'a, K, V> { db_iter: rocksdb::DBIterator<'a>, _phantom: PhantomData<(K, V)>, } impl<'a, K: DeserializeOwned, V: DeserializeOwned> Iter<'a, K, V> { pub(super) fn new(db_iter: rocksdb::DBIterator<'a>) -> Self { Self { db_iter, _phantom: PhantomData, } } } impl<'a, K: DeserializeOwned, V: DeserializeOwned> Iterator for Iter<'a, K, V> { type Item = (K, V); fn next(&mut self) -> Option<Self::Item> { let (key, value) = self.db_iter.next()?; let key = bincode::deserialize(&key[PREFIX_LEN..]).ok()?; let value = bincode::deserialize(&value).ok()?; Some((key, value)) } }感觉正常人都看不懂这个是什么。 回到泛型基础语法上来。先来看定义函数: Copyfn largest<T>(list: &[T]) -> &T { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest(&number_list); println!("The largest number is {}", result); let char_list = vec!['y', 'm', 'a', 'q']; let result = largest(&char_list); println!("The largest char is {}", result); }这里定义的largest函数,后面接了"<T>"这个还算正常,学过C++都知道这里是一个模板定义,或者说类型定义。后续T 代表着一种类型。 还可以出现在结构体中: Copystruct Point<T> { x: T, y: T, } impl<T> Point<T> { fn x(&self) -> &T { &self.x } } fn main() { let p = Point { x: 5, y: 10 }; println!("p.x = {}", p.x()); }这里"impl"也要加上"<T>" 以及枚举中,如典型的Result: Copyenum Result<T, E> { Ok(T), Err(E), }在后面有介绍生命周期,生命周期也类似类型,也需要放在这里'<>' 如: Copypub struct Iter<'a, K, V> { db_iter: rocksdb::DBIterator<'a>, _phantom: PhantomData<(K, V)>, }
  5. HelloWorldRust没隔6个星期,就会发布一个小版本。在Rust历史中又有三个大版本的差别。这里不是传统的x.y.z里面的版本号。 Rust以3年为一个界限,发布一个大版本,新特性可能不兼容之前版本。每个版本叫做一个edition。有点类似C98/C11/C14/C17 的感觉。目前Rust主要有2015/2018/2021三个Edition。 Rust环境安装通过rustup工具,可以帮助我们一键安装rust开发环境: Copycurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh Current installation options: default host triple: x86_64-apple-darwin default toolchain: stable (default) profile: default modify PATH variable: yes 1) Proceed with installation (default) 2) Customize installation 3) Cancel installation 直接Enter采用默认方式 Rust is installed now. Great! To get started you may need to restart your current shell. This would reload your PATH environment variable to include Cargo's bin directory ($HOME/.cargo/bin). To configure your current shell, run: source "$HOME/.cargo/env"这里执行下环境导入 Copysource "$HOME/.cargo/env"然后查看rust版本: Copyrustc --version rustc 1.70.0 (90c541806 2023-05-31)为当前最新的1.70.0。 Rust官方针对不同的IDE都有开发相关插件。比如Vim/Emacs,这里比较推荐实用VS Code。在VS Code界面安装 rust-analyzer插件。 这样即可舒服的写rust代码了。 创建工程创建一个demo目录,用于放工程文件,然后在这个目录中: Copycargo new --bin helloworld Created binary (application) `helloworld` package即可创建好项目。项目目录为: Copy. └── helloworld ├── Cargo.toml └── src └── main.rs然后用VScode打开工程,并打开这里的main.rs。 Copyfn main() { println!("Hello, world!"); }创建工程到时候,已经默认生成了打印"Hello, world!"。我们修改成: Copyuse std::env; fn main() { let name = env::args().skip(1).next(); match name { Some(n) => println!("Hello {}", n), None => println!("Please use ./hellowolrd name.") } }通过cargo可以构建: Copycargo build Compiling helloworld v0.1.0 (Solana-Asia-Summer-2023/s101/Solana-Rust/demo/helloworld) Finished dev [unoptimized + debuginfo] target(s) in 0.99s构建完成后,可执行二进制在: Copy./target/debug/helloworld abc Hello abc输入后可以直接执行,得到结果。 也可以直接通过cargo 来执行: Copycargo run abc Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/helloworld` Hello abc到这里就完成了HelloWorld过程了。 rust代码结构上面我们对模板代码做了简单修改。模板代码过于简单。所以我们加入了命令行参数并增加了一个其他语言中没有的关键字 match。 来看代码开头: Copyuse std::env;当要使用其他库的时候,首先用use来进行导入。这个有点类似其他语言的import/include等。 然后是main函数: Copyfn main() { ... }函数的定义是fn 开头,然后跟函数名,以及用()括起来的参数列表,最后是返回值类型,这里因为没有返回值,所以是空。再来看个函数定义: Copy// Function that returns a boolean value fn is_divisible_by(lhs: u32, rhs: u32) -> bool { // Corner case, early return if rhs == 0 { return false; } // This is an expression, the `return` keyword is not necessary here lhs % rhs == 0 }首先用fn表示函数定义。然后函数名为"is_divisible_by",参数为:lhs: u32, rhs: u32 这里又和一些语言不一样了。 格式为 参数名 : 类型 。最后通过 -> 分割返回值类型。 这里在函数定义逻辑为: Copy// Corner case, early return if rhs == 0 { return false; } // This is an expression, the `return` keyword is not necessary here lhs % rhs == 0这里和普通语言又有点不一样。 对于返回值,可以显示的调用reutrn : return false; 也可以通过表达式来实现,表达式不要以";"结尾: lhs % rhs == 0 表达式的结果,作为返回值,不需要加"return"。
  6. 课后练习实现一个 DApp 页面,实现代币创建,并按白名单发送空投 思路创建代币首先要构造一个 MintAccount: CopymintKeypair = Keypair.generate();然后创建一个创建这个 MintAccount 的指令,我们借助系统指令库 CopySystemProgram.createAccount({ fromPubkey: publicKey, newAccountPubkey: mintKeypair.publicKey, space: MINT_SIZE, lamports:lamports, programId: TOKEN_PROGRAM_ID, }),这里 from 是付费人,new 是要创建的地址,space 是 data 的大小,因为我们要放 MintAccount 其为 spl-token 库里的定义 lamports 我们通过 库提供的const lamports = await getMinimumBalanceForRentExemptMint(connection);来获得一个 Account 对应这个大小的存储空间的最小的 rent 花费。 接着创建 创建 token 的指令: CopycreateInitializeMint2Instruction(mintKeypair.publicKey, 9, publicKey, publicKey, TOKEN_PROGRAM_ID)这里依次是创建的 MintAccount,精度,mintAuthority,freezeAuthority 以及 Token 合约地址。 最后就是按照我们前面课程中的方式构造交易并发送。 这里要注意,因为我们创建了新的 MintAccount,其内容修改需要签名,因此在发送交易的时候,带上: Copyconst signature = await sendTransaction(trx, connection, { minContextSlot, signers:[mintKeypair], });Mint Token要 mint token,先要生成 ata 账号地址,spl token 的库里面有函数。 CopyataAccount = await getAssociatedTokenAddressSync( mintKeypair.publicKey, owner, false, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID );依次传入 MintAccount,为谁创建,后三个参数固定这样传即可。 然后判断这个 account 是否存在,可以用我们前面 rpc 的 getaccount,这里库也封装了函数 Copyawait getAccount(connection, ataAccount);没有的时候,会抛异常,我们要创建这个 ATA 账号 CopytxInstructions.push( createAssociatedTokenAccountInstruction( publicKey, ataAccount, owner, mintKeypair.publicKey, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID ) );调用库里的 createAssociatedTokenAccountInstruction 创建指令,依次传入付费的人,上面的 ata 账号,为谁创建以及 MintAccount。 如果存在则跳过 最后执行 Mint: CopytxInstructions.push( createMintToInstruction( mintKeypair.publicKey, ataAccount, publicKey, BigInt(toCount) ) );调用库函数 createMintToInstruction 传入 MintAccount,ata 账号以及 MintAuthority,因为只有他才有权限 mint,和数量。 然后就是跟前面一样,构造交易并发送。注意这次没有额外的 singer 要求了。 参考实现spl token exercise
  7. 合约调用在前面的例子中,我们通过 web3.js 提供的 SystemProgram 来帮助我们实现了转账的功能。 但是对于一个陌生的合约,我们要怎么来发起调用请求呢? 合约的入口这里我们以 SPL Token 合约来举例。SPL Token 合约类似 web3.js 一样,其实已经封装好了一套 JS 库给我们来直接使用。这里我们不使用库,而以一个前端的身份,来看这样的一个合约,我们要怎么来交互。 我们以 transfer 函数作为例子。 首先要理解合约的作用和参数,这个可以跟合约开发去沟通。比如我们从注释了解到 transfer 为 Copy/// Transfers tokens from one account to another either directly or via a /// delegate. If this account is associated with the native mint then equal /// amounts of SOL and Tokens will be transferred to the destination /// account. /// /// Accounts expected by this instruction: /// /// * Single owner/delegate /// 0. `[writable]` The source account. /// 1. `[writable]` The destination account. /// 2. `[signer]` The source account's owner/delegate. /// /// * Multisignature owner/delegate /// 0. `[writable]` The source account. /// 1. `[writable]` The destination account. /// 2. `[]` The source account's multisignature owner/delegate. /// 3. ..3+M `[signer]` M signer accounts. Transfer { /// The amount of tokens to transfer. amount: u64, },总共需要 3 个 key,分别是,发送方,接收方以及发送方的 ower/delegate。然后有一个类型 u64 的参数。 知道了这些我们才可以构造我们的 Instruction。Instruction 的定义为: Copy/** * Transaction Instruction class */ export class TransactionInstruction { /** * Public keys to include in this transaction * Boolean represents whether this pubkey needs to sign the transaction */ keys: Array<AccountMeta>; /** * Program Id to execute */ programId: PublicKey; /** * Program input */ data: Buffer; constructor(opts: TransactionInstructionCtorFields); }所以我们主要就是要从合约的定义中知道这里的 keys 是什么, data 是什么,programId 自然就是合约的地址。 构造 Instruction在上面,我们知道了 Instruction 的定义。那么要如何来构造呢? 如果你是用 TypeScript,那么比较醒目。keys 是 AccountMeta 的数组,AccountMeta 的定义为: Copy/** * Account metadata used to define instructions */ type AccountMeta = { /** An account's public key */ pubkey: PublicKey; /** True if an instruction requires a transaction signature matching `pubkey` */ isSigner: boolean; /** True if the `pubkey` can be loaded as a read-write account. */ isWritable: boolean; };总共就三个成员,一个 PublicKey 表示 Account 的地址, 一个 isSigner 表示是否为签名者,说白了就是是不是你自己。以及 isWritable,表示这个 Account 的 Data 部分是否可以修改。 这里 PublicKey 的定义为: Copyexport class PublicKey extends Struct { /** * Create a new PublicKey object * @param value ed25519 public key as buffer or base-58 encoded string */ constructor(value: PublicKeyInitData); ... } /** * Value to be converted into public key */ type PublicKeyInitData = number | string | Uint8Array | Array<number> | PublicKeyData;其实就是用公钥的字符串就可以进行构造了。 所以如果是用 TypeScript。就严格按照类型来定义就好了。 如果是 Javascript,可以用字典来进行显式初始化: 而 data 部分是一个 Buffer,其实本质是一段二进制,其格式是根据合约来定义的,也可以参考标准,比如"Anchor"。而 SPL Token 的二进制定义为: 这里我们可以借助 web.js 提供的"encodeData"方法来进行序列化。而 web3.js 的指令定义依赖了 solana 提供的 buffer-layout,因此需要这样来定义: 这样实际上就是定义了上面的这个序列化的图。当调用encodeData方法时,就可以按照这里定义的格式进行序列化了。 构造 Transaction有了 TransactionInstruction 之后,就可以构造 Transaction 了。前面已经说过,现在用的是 VersionedTransaction。他的定义为: Copyexport class VersionedTransaction { signatures: Array<Uint8Array>; message: VersionedMessage; get version(): TransactionVersion; constructor(message: VersionedMessage, signatures?: Array<Uint8Array>); serialize(): Uint8Array; static deserialize(serializedTransaction: Uint8Array): VersionedTransaction; sign(signers: Array<Signer>): void; addSignature(publicKey: PublicKey, signature: Uint8Array): void; }可以通过一个 VesionedMessage 来构建,定义为: Copytype VersionedMessage = Message | MessageV0; export const VersionedMessage: { deserializeMessageVersion(serializedMessage: Uint8Array): 'legacy' | number; deserialize: (serializedMessage: Uint8Array) => VersionedMessage; };Message 是为了兼容以前的 Message,现在的都是用 MessageV0: Copyexport class MessageV0 { header: MessageHeader; staticAccountKeys: Array<PublicKey>; recentBlockhash: Blockhash; compiledInstructions: Array<MessageCompiledInstruction>; addressTableLookups: Array<MessageAddressTableLookup>; constructor(args: MessageV0Args); get version(): 0; get numAccountKeysFromLookups(): number; getAccountKeys(args?: GetAccountKeysArgs): MessageAccountKeys; isAccountSigner(index: number): boolean; isAccountWritable(index: number): boolean; resolveAddressTableLookups(addressLookupTableAccounts: AddressLookupTableAccount[]): AccountKeysFromLookups; static compile(args: CompileV0Args): MessageV0; serialize(): Uint8Array; private serializeInstructions; private serializeAddressTableLookups; static deserialize(serializedMessage: Uint8Array): MessageV0; }看上去超级复杂。因此 web3.js 给我们提供了一个简单的方法,通过TransactionMessage来构造: Copyexport class TransactionMessage { payerKey: PublicKey; instructions: Array<TransactionInstruction>; recentBlockhash: Blockhash; constructor(args: TransactionMessageArgs); static decompile(message: VersionedMessage, args?: DecompileArgs): TransactionMessage; compileToLegacyMessage(): Message; compileToV0Message(addressLookupTableAccounts?: AddressLookupTableAccount[]): MessageV0; }其compileToV0Message可以转换道得到对应的 MessageV0。 因此只需要提供 TransactionMessageArgs 即可,其定义为: Copytype TransactionMessageArgs = { payerKey: PublicKey; instructions: Array<TransactionInstruction>; recentBlockhash: Blockhash; }; /** * Blockhash as Base58 string. */ type Blockhash = string;终于到正主了,这里我们看到 payerKey 是付 gas 人的地址。instructions 是我们前面介绍的 Instruction。 recentBlockhash 是最近的 Blockhash 这个不能太久远。可以通过 RPC 进行请求。 这样我们连起来就是: Copyconst txInstructions = const message = new TransactionMessage({ payerKey: this.keypair.publicKey, recentBlockhash: latestBlockhash.blockhash, instructions: txInstructions }).compileToV0Message(); const trx = new VersionedTransaction(messageV0);构造 SPL Token 的 转账交易前面我们已经搞清楚了 SPL Token 合约转账指令的结构,3 个账号一个数目。账号比较容易。我们自己账号对应的 SPL Token 的 ATA 账号,对方接收的账号。这两个都是不需要前面的,并且需要修改的。还有个我们自己的 SOL 账号,这个需要签名。 先看下 Token 合约的 Transfer 定义: Copy/// Transfers tokens from one account to another either directly or via a /// delegate. If this account is associated with the native mint then equal /// amounts of SOL and Tokens will be transferred to the destination /// account. /// /// Accounts expected by this instruction: /// /// * Single owner/delegate /// 0. `[writable]` The source account. /// 1. `[writable]` The destination account. /// 2. `[signer]` The source account's owner/delegate. /// /// * Multisignature owner/delegate /// 0. `[writable]` The source account. /// 1. `[writable]` The destination account. /// 2. `[]` The source account's multisignature owner/delegate. /// 3. ..3+M `[signer]` M signer accounts. Transfer { /// The amount of tokens to transfer. amount: u64, },按照上面说的,我们依靠 web3.js 提供的 buffer-layout 我们来定义这个 transfer 的指令。 Copyexport interface TransferInstructionData { instruction: TokenInstruction.Transfer; amount: bigint; } /** TODO: docs */ export const transferInstructionData = struct<TransferInstructionData>([u8('instruction'), u64('amount')]);这里比 Rust 的定义,多了个"instruction",这个是因为 Token 的序列化规则,使用一个 u8 来表示是那个指令。 定义好指令,我们就可以开始构建了。 按照上面说先构建指令: Copyfunction createTransferInstruction( source, destination, owner, amount, programId ) { const keys = [ { pubkey: source, isSigner: false, isWritable: true }, { pubkey: destination, isSigner: false, isWritable: true }, { pubkey: owner, isSigner:true, isWritable: false} ]; const data = Buffer.alloc(9); data.writeUInt8(3); const bigAmount = BigInt(amount); data.writeBigInt64LE(bigAmount,1) return new TransactionInstruction({ keys, programId, data }); }这里的第一个 byte 为 3 表示 transfer 指令。 然后构建交易: Copyconst txInstructions = [ createTransferInstruction( ATA_PUBKEY_KEY, TO_PUBLIC_KEY, publicKey, toCount, TOKEN_PROGRAM_ID ), ]; const { context: { slot: minContextSlot }, value: { blockhash, lastValidBlockHeight }, } = await connection.getLatestBlockhashAndContext(); const messageV0 = new TransactionMessage({ payerKey: publicKey, recentBlockhash: blockhash, instructions: txInstructions, }).compileToV0Message(); const trx = new VersionedTransaction(messageV0);最后利用前面学的通过钱包来发送交易: Copyconst signature = await sendTransaction(trx, connection, { minContextSlot, }); console.log("signature:", signature);这样我们就完成了通过前端来和特定的合约进行交互。 Demospl token demo
  8. 通过 WalletAdatper 与钱包交互为了给 DApp 提供一套统一的兼容钱包的接口。Solana 设计了一套 Wallet Adapter。 Solana 要求,钱包方需要按照该套接口设计,提供实现。这样 DApp 使用方,只需要按照一套接口,就可以轻松支持多个钱包。接口包含了 网络选择 账号选择 账号签名 等 除了统一的接口,Adapter 还设计了一套基础 UI,其包括了弹出钱包的选择列表,以及链接钱包后的的账号地址显示。 安装在你的工程总安装 Wallet_Adapter 依赖: Copynpm install --save \ @solana/wallet-adapter-base \ @solana/wallet-adapter-react \ @solana/wallet-adapter-react-ui \ @solana/wallet-adapter-wallets \ @solana/web3.js这里我们还会用到一些 web3.js 里面的变量,所以也将其 install 上。 在使用地方先 import 相关 SDK Copyimport { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react'; import { WalletAdapterNetwork } from '@solana/wallet-adapter-base'; import { WalletModalProvider, WalletDisconnectButton, WalletMultiButton } from '@solana/wallet-adapter-react-ui'; import { clusterApiUrl } from '@solana/web3.js';这里因为我们的示例 demo 是 react 的,所以使用了 react-ui,Wallet-adapter 同时也提供了 Material UI 和 Ant Design 的组件。 已经实现了 Adapter 的钱包参见列表。 这里我们使用: Copyimport {SolongWalletAdapter} from '@solana/wallet-adapter-solong' import {PhantomWalletAdapter} from '@solana/wallet-adapter-phantom';链接钱包链接钱包的步骤,是在用户界面设置一个"Connect"的按钮,当点击时,弹出一个钱包选择 list 界面。可使用钱包,通过数组参数参数。 Copythis.network = WalletAdapterNetwork.Testnet; // You can also provide a custom RPC endpoint. this.endpoint = clusterApiUrl(this.network); this.wallets =[ new SolongWalletAdapter(), new PhantomWalletAdapter(), ];然后再弹出 UI 将钱包罗列出来 Copy<ConnectionProvider endpoint={endpoint}> <WalletProvider wallets={wallets} autoConnect> <WalletModalProvider> <WalletMultiButton /> <WalletDisconnectButton /> </WalletModalProvider> </WalletProvider> </ConnectionProvider>这里主要使用了 ConnectionProvider 来指定相应的网络。endpoint 参数为使用的 RPC 环境。通过 WalletProvider 来选择实现了 Adapter 的插件钱包,示例中我们设置了 Phantom。 最后在 WalletModalProvider 通过相应的按钮触发对钱包的选择。也就是我们上面传递的 Solong 和 Phantom。 当用户点击 WalletMultiButton 的时候,会自动弹出钱包选择界面。选择钱包后,会弹出钱包的链接界面。当用户点击链接后,这里的 ModalProvider 会得到选择的账号的信息,并将地址显示在按钮上。 当点击 WalletDisconnectButton 后,会断开链接。 发送请求前面介绍了 web3.js 的使用,在发送请求的时候,我们需要用账号的私钥对交易内容做签名。那么在使用钱包的情况下该如何操作呢? 首先 import 相关库 Copyimport { WalletNotConnectedError } from '@solana/wallet-adapter-base'; import { useConnection, useWallet } from '@solana/wallet-adapter-react'; import { Keypair, SystemProgram, Transaction } from '@solana/web3.js'; import React, { FC, useCallback } from 'react';然后先取出链接和公钥: Copyconst { connection } = useConnection(); const { publicKey, sendTransaction } = useWallet();这里通过 useConnection 可以得到我们前面钱包里面选择的 RPC 链接,useWallet 返回的结果为选中的钱包的地址,以及使用该钱包发送交易的方法。 Copyconst { context: { slot: minContextSlot }, value: { blockhash, lastValidBlockHeight } } = await connection.getLatestBlockhashAndContext(); const signature = await sendTransaction(transaction, connection, { minContextSlot });通过 connection.getLatestBlockhashAndContext 可以得到 minContextSlot 信息,然后再调用 sendTransaction 方法,就可以出发钱包弹出 UI,并提示用户确认,当用户点击确认后,既完成请求的发送。 切换账号如果用户需要切换账号,那么通过 UI 提供的 Disconnect 入口,先取消当前账号的链接。然后再通过链接界面,选择其他的钱包账号。所以切换账号就是先断开,再重新链接的过程。 取消链接,只需要删除当前记录的用户即刻。 而切换账号则可以直接在此使用"连接" 的流程。 Demowallet adapter demo
  9. Web3.js如果之前接触过 ETH,一定知道 ETH 的web3.js ,主要提供了通过 JavaScript 与 ETH 上合约进行交互。而 Solana 也提供了与 Solana 的 JSON RPC 接口交互的solana-web3.js 。通过这个库,可以实现在 dapp 中用 JavaScritp 和 Solana 上的智能合约进行交互。 Web3.js 库主要分为三部分 RPC 访问 keypair 管理 交易发送 Demo首先来看一个 Demo,在 Demo 中,首先导入私钥,然后可以查询该账号的余额。然后执行转账,转账后再可以查询余额来进行结果判断。 这里我们是以开发网为开发环境,所以相应的数据都在开发网上。 安装solana/web3.js 提供了 npm 包和 yarn 包的选择,使用 yarn 的话,可以在工程中用 Copyyarn add @solana/web3.js进行引用,如果使用的 npm 的话,使用: Copynpm install --save @solana/web3.js如果想在自己已有的没有使用包管理的项目中使用,可以先 checkout 出 solana/web3.js 的代码,然后 checkout 到最新的分支上,执行: Copycd your_solana_web3js_directory git clone https://github.com/solana-labs/solana-web3.js.git git checkout v1.78.0 yarn install yarn build从 lib 目录取出 index.iife.js 既为浏览器使用的版本,然后使用<scritp />进行引用。 或者用已经编译好的: Copy<!-- Production (minified) --> <script src="https://unpkg.com/@solana/web3.js@latest/lib/index.iife.min.js"></script>ConnectionWeb3.js 通过 Connection 来抽象一个 RPC 链接。通过 Copylet url = 'https://api.devnet.solana.com'; rpcConnection = new Connection(url);通过指定 RPC 的地址这样来创建。这个对象包含了所有的 RPC 方法: 可以查看每个方法的文档,来查看使用方法。这里举几个简单的例子。比如获取当前区块高度。 Copylet latestBlockhash = await this.connection.getLatestBlockhash('finalized'); console.log(" - Fetched latest blockhash. Last Valid Height:", latestBlockhash.lastValidBlockHeight);这里指定了区块状态为 finalized,在 console 中可以看到: Copy - Fetched latest blockhash. Last Valid Height: 175332530账号早年版本(20 年),Web3.js 提供了一个 Account 对象,但是后来将其抽象为一个 Keypair 对象。 keypair 在前面的章节中有介绍到。其本质就是一个私钥和公钥的组合。因此 keypair 可以用一段私钥来进行初始化: Copyconstructor(keypair?: Ed25519Keypair)可以通过构造函数,直接创建。或者通过 Copystatic fromSecretKey(secretKey: Uint8Array, options?: { skipValidation?: boolean; })来创建。 在前面的章节中,通过命令行创建了私钥,在文件"~/.config/solana/id.json"中,没有加密的情况下可以直接取出来 Copylet secretKey = Uint8Array.from(JSON.parse('[24,xxx,119]')); const keypair = Keypair.fromSecretKey(secretKey); console.log("address:", keypair.publicKey.toString())可以看到: Copyaddress: 5pWae6RxD3zrYzBmPTMYo1LZ5vef3vfWH6iV3s8n6ZRG和我们命令行中的地址是一样的。 这里 publicKey 就是对应的账号地址,keypair 就是 Signer。 发送交易在介绍 Solana 核心概念的时候,我们有介绍到 Instruction 和 Transaction 以及 Message。所以发送交易,就是构建 Instructions 数组,然后构造 Message,再放到 Transaction 里面,做签名并进行发送。 如果是普通应用合约,需要自己封装 Instruction。 Copy/** * Transaction Instruction class */ export class TransactionInstruction { /** * Public keys to include in this transaction * Boolean represents whether this pubkey needs to sign the transaction */ keys: Array<AccountMeta>; /** * Program Id to execute */ programId: PublicKey; /** * Program input */ data: Buffer; constructor(opts: TransactionInstructionCtorFields); }其中 programId 表示调用合约的地址。key 是合约中需要使用到的 Account, data 则是所有的输入序列化后的二进制。 因为合约的入口是: Copydeclare_process_instruction!( process_instruction, DEFAULT_COMPUTE_UNITS, |invoke_context| { let transaction_context = &invoke_context.transaction_context; let instruction_context = transaction_context.get_current_instruction_context()?; let instruction_data = instruction_context.get_instruction_data(); let instruction = limited_deserialize(instruction_data)?;可以简化为: Copyfn process_instruction( program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8], ) -> ProgramResult {具体的方法是从 data 里面解析出来,然后再解析出来参数。 而 Solana 的系统合约,或者说 Native Program。 Web3.js 已经为我们封装好了一些 Instruction。比如转账: CopySystemProgram.transfer({ fromPubkey:new PublicKey(this.state.publicKey), //this.publicKey, toPubkey: new PublicKey(this.state.toPublicKey),//destination, lamports: this.state.toCount,//amount, })这里表示从 fromPubkey 地址转 lamports 的 SOL 到 toPubkey 的地址。他实际上会调用"11111111111111111111111111111111"合约的 transfer方法。该方法接受三个参数,其中 fromPubkey 需要是签名对象。 Copyfn transfer( from_account_index: IndexOfAccount, to_account_index: IndexOfAccount, lamports: u64, invoke_context: &InvokeContext, transaction_context: &TransactionContext, instruction_context: &InstructionContext, ) -> Result<(), InstructionError> {因为转账只需要用到一个 Instruction,所以用这个 Instrcuton 构造 Message: Copyconst messageV0 = new TransactionMessage({ payerKey: this.keypair.publicKey, recentBlockhash: latestBlockhash.blockhash, instructions: txInstructions }).compileToV0Message();这里 instructions 是 Array<TransactionInstruction>一个数组。 payerKey 则是发送这个消息的 gas 付费者,其也需要提供签名。 recentBlockhash 通过我们前面的 RPC 可以获取到。这里 recentBlockhash 不能隔的太远。这样就限制了消息的签名时间。最后调用 compileToV0Message 构造 Message 对象。 有了 Message,还有构造 VersionedTransaction, 早期的 Transaction 已经废弃。 Copyexport class VersionedTransaction { signatures: Array<Uint8Array>; message: VersionedMessage; get version(): TransactionVersion; constructor(message: VersionedMessage, signatures?: Array<Uint8Array>); serialize(): Uint8Array; static deserialize(serializedTransaction: Uint8Array): VersionedTransaction; sign(signers: Array<Signer>): void; addSignature(publicKey: PublicKey, signature: Uint8Array): void; }新的 VersionedTransaction 对象,通过传入 VersionedMessage 来构造: Copyconstructor(message: VersionedMessage, signatures?: Array<Uint8Array>);这里我们上面构造的 V0 就是 VersionedMessage 的对象。 这里可以传入 signatures,比如通过硬件钱包签名的内容。或者不传入也可以,调用: Copysign(signers: Array<Signer>): void;传入我们上面的 keypair。也可以对 VersionedTransaction 进行签名。 构造结束后,通过 connection 的 sendTransaction 方法发送即可: CopysendTransaction(transaction: VersionedTransaction, options?: SendOptions): Promise<TransactionSignature>;这里返回的 TransactionSignature 即为,交易的 hash,可以通过浏览器进行查询。 Demoweb3 ui demo
  10. 课后练习通过curl和wscat命令行来模拟一个监视钱包动作 提示: 创建一个新账号 实时展示余额变化 列出已知SPL-Token的余额 实时展示SPL-Token余额变化 创建账号我们复用前面的账号 SOL账号: Czorr4y9oFvE3VdfCLVFuKDYxaNUG1iyQomR7kMZUuzi SPL-Token(Mint Account): 7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9 Token Account: EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AK 订阅SOL余额变化这个其实已经在课上演示过了。详情可以查看视频录像以及教程中的"订阅Account变化"章节 展示SPL-Token变化早期的钱包是通过官方的 token-list 来获得 已知的SPL-Token,现在则通过Metaplex的FT标准查询。除此之外还可以通过订阅Token合约管理的账户变化 来判断是否有Owner为自己的 Token Account被创建。 这里我们假设第一种情况,钱包只维护知名token或者用户自己添加的Token,比如上面的 "7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9" 我们首先获取这个SPL-Token下我们有多少 Token Account: Copycurl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getTokenAccountsByOwner", "params": [ "Czorr4y9oFvE3VdfCLVFuKDYxaNUG1iyQomR7kMZUuzi", { "mint": "7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9" }, { "encoding": "jsonParsed" } ] } '这里根据结果发现只有一个 "EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AK" 那么我们只需要按照教程里面,订阅这个Account的变化就可以了。如果有多个,那么就订阅多个。 在重复对其他 SPL-Token做同样操作,既可以完成SPL-Token钱包的功能。 首先建立websocket链接,并发起对这个Account的订阅: Copywscat -c wscat -c wss://api.devnet.solana.com --proxy=http://127.0.0.1:1087 Connected (press CTRL+C to quit) > {"jsonrpc":"2.0","id":1,"method":"accountSubscribe","params":["EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AK",{"encoding":"jsonParsed","commitment":"finalized"}]}然后再另一个终端,用"spl-token"命令行来进行转账: Copyspl-token transfer --fund-recipient 7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9 1 BBy1K96Y3bohNeiZTHuQyB53LcfZv6NWCSWqQp89TiVu Transfer 1 tokens Sender: EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AK Recipient: BBy1K96Y3bohNeiZTHuQyB53LcfZv6NWCSWqQp89TiVu Recipient associated token account: H1jfKknnnyfFGYPVRd4ZHwUbXLF4PbFSWSH6wMJq6EK9 Signature: 3paamDSKFk5depKufcDjmJ8wc3eXte3qcgtitFu4TyDi8z9GTXMrLGEgPHgQMnAzFBXYoWxyF5JFzA54Fjvi2ZUK接着我们就可以在前面的监听中收到: Copy< {"jsonrpc":"2.0","method":"accountNotification","params":{"result":{"context":{"slot":236334118},"value":{"lamports":2039280,"data":{"program":"spl-token","parsed":{"info":{"isNative":false,"mint":"7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9","owner":"Czorr4y9oFvE3VdfCLVFuKDYxaNUG1iyQomR7kMZUuzi","state":"initialized","tokenAmount":{"amount":"92000000000","decimals":9,"uiAmount":92.0,"uiAmountString":"92"}},"type":"account"},"space":165},"owner":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","executable":false,"rentEpoch":0,"space":165}},"subscription":18067841}}可以看到当前余额为92了。我们在用"balance"确认下 Copyspl-token balance 7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9 92
  11. 接口RPC节点相关接口获取集群节点信息通过getClusterNodes方法可以获得当前网络内,集群节点的相关信息,比如验证者的key,节点IP,节点版本等。 Copycurl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getClusterNodes" } '得到的输出类似这样: Copy{ "jsonrpc": "2.0", "result": [ { "featureSet": 2891131721, "gossip": "67.209.54.46:8001", "pubkey": "8pgVP32abaxodvpJx3iXo4o9FUWzarudQ7RHZAkkqEKi", "pubsub": null, "rpc": null, "shredVersion": 28353, "tpu": "67.209.54.46:8004", "tpuQuic": "67.209.54.46:8010", "version": "1.16.2" } ... ] }从结果字段名,也可以比较直观的推出这些字段的意义 区块相关接口获取当前区块高度通过getBlockHeight可以获取当前的区块高度 Copycurl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d ' { "jsonrpc":"2.0","id":1, "method":"getBlockHeight" } '得到输出: Copy{ "jsonrpc": "2.0", "result": 174302040, "id": 1 }可以看到,当前测试网的高度到了174302040。 获取最近的Block Hash通过getLatestBlockhash可以获得连上最近的一个Block的Hash值和高度 Copycurl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d ' { "id":1, "jsonrpc":"2.0", "method":"getLatestBlockhash", "params":[ { "commitment":"processed" } ] } '得到结果: Copy{ "jsonrpc": "2.0", "result": { "context": { "apiVersion": "1.16.2", "slot": 207172864 }, "value": { "blockhash": "2rSgjtXjKDcMYZTdSErwSz9bPXota73uecdJXUxEz2a5", "lastValidBlockHeight": 174481567 } }, "id": 1 }这里根据字面意思,可以看到最近的一个区块的slot,hash以及block height。 获取指定高度block的信息获取指定高度block的信息,通过getBlock方法。如 Copycurl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0","id":1, "method":"getBlock", "params": [ 174302734, { "encoding": "jsonParsed", "maxSupportedTransactionVersion":0, "transactionDetails":"full", "rewards":false } ] } '这里结果太多,不再罗列。在请求中,我们加入了 "encoding": "jsonParsed",将结果按照json的格式 进行展示。transactionDetails 设置返回的交易信息的内容复杂等级,设置有"full","accounts","signatures","none", 默认是"full"。maxSupportedTransactionVersion这个参数和后面介绍的带版本号的交易有关,表示返回最大的版本号,当前 可以传0即可,默认也是0。布尔值rewards表示是否携带rewards信息。 获取指定block的确认状态有时候在上面获得当前最高区块,但是查询区块信息的时候却又查询不到,这里可以通过getBlockCommitment查看下对应区块的状态。 Copycurl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getBlockCommitment", "params":[174302734] } '得到结果: Copy{ "jsonrpc": "2.0", "result": { "commitment": null, "totalStake": 144333782793465543 }, "id": 1 }这里totalStake表示提交确认的节点总共Stake的SOL数目,也就是POS的权重。如果commitment不为null的时候,将是一个数组 表示各个集群中Stake的数量分布。 一次性获取多个Block的信息前面的getBlock获得了单个Block的信息,还可以通过getBlocks一次性获得多个Block的信息。 Copycurl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getBlocks", "params": [ 174302734, 174302735 ] } '其余参数都是一样的,这里参数中,前面的部分是block number的数组 分页获取Block前面两个获取Block信息的方法,分别可以获得单个Block和多个指定Block号的信息。因为Block Number是递增且不一定连续的,因此 还Solana还提供了一个分页查询的方式getBlocksWithLimit,从起始号查询多少个。 Copycurl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id":1, "method":"getBlocksWithLimit", "params":[174302734, 3] } '得到: Copy{ "jsonrpc": "2.0", "result": [ 174302734, 174302735, 174302736 ], "id": 1 }三个BlockNumber,接着我们可以前面的GetBlocks来获得这三个Block的详细信息。 Slot和Epoch相关接口获取当前Epoch信息首先Epoch是什么,在前面也有介绍到,epoch在一般POS中比较常见,表示这个周期内,一些参与验证的节点信息是固定的,如果有新 节点或者节点权重变更,将在下一个epoch中生效。 Copycurl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d ' {"jsonrpc":"2.0","id":1, "method":"getEpochInfo"} '输出类似: Copy{ "jsonrpc": "2.0", "result": { "absoluteSlot": 207170348, "blockHeight": 174478875, "epoch": 492, "slotIndex": 150092, "slotsInEpoch": 432000, "transactionCount": 258177341740 }, "id": 1 }里面有当前周期的区块高度,slot数目,以及transaction的数目。 而getEpochSchedule方法则是获取Epoch的调度信息, Copycurl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d ' { "jsonrpc":"2.0","id":1, "method":"getEpochSchedule" } '可以看到输出中: Copy{ "jsonrpc": "2.0", "result": { "firstNormalEpoch": 14, "firstNormalSlot": 524256, "leaderScheduleSlotOffset": 432000, "slotsPerEpoch": 432000, "warmup": true }, "id": 1 }从字面意思也能看到,这里有Epoch中slot的数目,起始值等信息。 获取最新Slot和Epoch类似,可以获得当前的Slot: Copycurl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d ' {"jsonrpc":"2.0","id":1, "method":"getSlot"} '直接得到slot值: Copy{"jsonrpc":"2.0","result":209119756,"id":1}账号相关接口获取Account信息第一章有介绍,Solana上存储的内容,都是一个Account对象,有基础的元数据信息: Copypub struct Account { /// lamports in the account pub lamports: u64, /// data held in this account #[serde(with = "serde_bytes")] pub data: Vec<u8>, /// the program that owns this account. If executable, the program that loads this account. pub owner: Pubkey, /// this account's data contains a loaded program (and is now read-only) pub executable: bool, /// the epoch at which this account will next owe rent pub rent_epoch: Epoch, }我们可以通过getAccountInfo RPC请求来查看,比如查看我们前面的测试账号: Copycurl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getAccountInfo", "params": [ "5pWae6RxD3zrYzBmPTMYo1LZ5vef3vfWH6iV3s8n6ZRG", { "encoding": "base58", "commitment": "finalized" } ] } '这里我们通过curl来直接发起HTTP请求,最直观的看发生什么。请求中我们指定了测试网的RPC地址。 https://api.devnet.solana.com 得到 Copy{ "jsonrpc": "2.0", "result": { "context": { "apiVersion": "1.16.1", "slot": 206885329 }, "value": { "data": [ "", "base58" ], "executable": false, "lamports": 59597675320, "owner": "11111111111111111111111111111111", "rentEpoch": 349, "space": 0 } }, "id": 1 }在result里面可以看到value里面的值项目,和Rust的结构体是一一对应的,其中data表示数据内容, 这里我们的普通账号不是合约账号,因此其为空,后面的"base58"表示如果这里有值,那么他将是二进制 内容的base58格式编码。这个编码格式是我们在请求里面的"encoding"来指定的。"executable"表示 是否为可执行合约,"lamports"表示余额,这里精度*10^9。所有普通账号的Owner都是系统根账号: "11111111111111111111111111111111"。 获取账号余额在上面的Account信息里面,我们已经可以知道账号余额lamports了,同时RPC还提供了getBalance可以更 简洁的得到余额信息: Copycurl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getBalance", "params": [ "5pWae6RxD3zrYzBmPTMYo1LZ5vef3vfWH6iV3s8n6ZRG" ] } '得到: Copy{ "jsonrpc": "2.0", "result": { "context": { "apiVersion": "1.16.1", "slot": 206886725 }, "value": 989995000 }, "id": 1 }可以看到是989995000,因为SOL的精度是10^9.所以也就是0.989995个SOL。 获取某个合约管理的所有Account类似Linux查询某个用户所有的文件。Solana提供了一个查询owener为某个合约的RPC方法。该方法的作用就是罗列出 某个合约管理的Account,比如SPL Token合约记录的所有用户的余额信息。 Copycurl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getProgramAccounts", "params": [ "namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX", { "encoding": "jsonParsed", "filters": [ { "dataSize": 128 } ] } ] } '获取所有NameService服务管理的名字且记录空间大小为128字节的记录: Copy{"jsonrpc":"2.0","result":[{"account":{"data":["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADHgMUi7LJb6+YQzBNlYJYu4QoAPOPzOY6F9NasCG9howAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaGVsbG8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","base64"],"executable":false,"lamports":1781761,"owner":"namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX","rentEpoch":349,"space":128},"pubkey":"5mBDoMGJvQTQhgAK2LtjKmG3TGV8J1m3LoEHRMXqits9"},{"account":{"data": ... {"account":{"data":["AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADHgMUi7LJb6+YQzBNlYJYu4QoAPOPzOY6F9NasCG9howAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaGVsbG8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","base64"],"executable":false,"lamports":1781761,"owner":"namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX","rentEpoch":349,"space":128},"pubkey":"8worhyBqrHu1MYYQdQ3zpg5ByuhUge4rYHHhN8E8Vc3j"}],"id":1}这里的data还需要用相应的序列化方法进行解析才能知道具体的记录是什么。 SPL-Token相关接口按照需求查询账号我们知道SPL Token的结构为: Copypub struct Account { /// The mint associated with this account pub mint: Pubkey, /// The owner of this account. pub owner: Pubkey, /// The amount of tokens this account holds. pub amount: u64, /// If `delegate` is `Some` then `delegated_amount` represents /// the amount authorized by the delegate pub delegate: COption<Pubkey>, /// The account's state pub state: AccountState, /// If is_native.is_some, this is a native token, and the value logs the rent-exempt reserve. An /// Account is required to be rent-exempt, so the value is used by the Processor to ensure that /// wrapped SOL accounts do not drop below this threshold. pub is_native: COption<u64>, /// The amount delegated pub delegated_amount: u64, /// Optional authority to close the account. pub close_authority: COption<Pubkey>, }我们可以查询某个Token下,所有owner为某人的Token账号,或者delegate为某人的所有账号。 Copycurl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getTokenAccountsByOwner", "params": [ "Czorr4y9oFvE3VdfCLVFuKDYxaNUG1iyQomR7kMZUuzi", { "mint": "7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9" }, { "encoding": "jsonParsed" } ] } '这里查询到这个token:7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9 ower为CnjrCefFBHmWnKcwH5T8DFUQuVEmUJwfBL3Goqj6YhKw所有账号。 Copy{ "jsonrpc": "2.0", "result": { "context": { "apiVersion": "1.16.5", "slot": 234689258 }, "value": [ { "account": { "data": { "parsed": { "info": { "isNative": false, "mint": "7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9", "owner": "Czorr4y9oFvE3VdfCLVFuKDYxaNUG1iyQomR7kMZUuzi", "state": "initialized", "tokenAmount": { "amount": "99000000000", "decimals": 9, "uiAmount": 99.0, "uiAmountString": "99" } }, "type": "account" }, "program": "spl-token", "space": 165 }, "executable": false, "lamports": 2039280, "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "rentEpoch": 0, "space": 165 }, "pubkey": "EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AK" } ] }, "id": 1 }而通过: Copycurl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getTokenAccountsByDelegate", "params": [ "Czorr4y9oFvE3VdfCLVFuKDYxaNUG1iyQomR7kMZUuzi", { "mint": "7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9" }, { "encoding": "jsonParsed" } ] } '因为我们没有设置代理操作。所以这里得到的结果为空。 获取某个Token Account账号的余额查询SPL Token的余额,有个ATA账号需要了解。本质上就是对应Token的账号: Copycurl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getTokenAccountBalance", "params": [ "EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AK" ] } '返回的值,会列出数量: Copy{ "jsonrpc": "2.0", "result": { "context": { "apiVersion": "1.16.3", "slot": 209132550 }, "value": { "amount": "99000000000", "decimals": 9, "uiAmount": 99.0, "uiAmountString": "99" } }, "id": 1 }这里可以看到,uiAmount是可以显示的数量,做了精度转换的。精度和真实amount都有列出来。 交易相关接口获取交易手续费针对某个交易,需要预估其手续费时,可以借助节点的预计算: Copycurl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d ' { "id":1, "jsonrpc":"2.0", "method":"getFeeForMessage", "params":[ "AQABAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQAA", { "commitment":"processed" } ] } '得到: Copy{ "jsonrpc": "2.0", "result": { "context": { "apiVersion": "1.16.3", "slot": 209111155 }, "value": null }, "id": 1 }这里参数中的字符串,是Transaction打包后的结果。也就是RawTransaction的序列化结果。 获取交易详细信息查询某个交易的详细信息: Copycurl https://api.devnet.solana.com -X POST -H "Content-Type: application/json" -d ' { "jsonrpc": "2.0", "id": 1, "method": "getTransaction", "params": [ "2o9qCEhwKi8w7hTFQJTLwMBZPFH8qM3iNd9rprtdY6XShyrpsqkWt4Df3Zgsxv6y4nbRe4SDgU8KMvuMfs7HxVhp", "jsonParsed" ] } '可以看到,结果跟浏览器中的结果基本是对应的: Copy{ "jsonrpc": "2.0", "result": { "blockTime": 1674954447, "meta": { "computeUnitsConsumed": 12481, "err": null, "fee": 5000, "innerInstructions": [], "logMessages": [ "Program gSbePebfvPy7tRqimPoVecS2UsBvYv46ynrzWocc92s invoke [1]", "Program gSbePebfvPy7tRqimPoVecS2UsBvYv46ynrzWocc92s consumed 2633 of 600000 compute units", "Program gSbePebfvPy7tRqimPoVecS2UsBvYv46ynrzWocc92s success", "Program gSbePebfvPy7tRqimPoVecS2UsBvYv46ynrzWocc92s invoke [1]", "Program gSbePebfvPy7tRqimPoVecS2UsBvYv46ynrzWocc92s consumed 5562 of 597367 compute units", "Program gSbePebfvPy7tRqimPoVecS2UsBvYv46ynrzWocc92s success", "Program gSbePebfvPy7tRqimPoVecS2UsBvYv46ynrzWocc92s invoke [1]", "Program gSbePebfvPy7tRqimPoVecS2UsBvYv46ynrzWocc92s consumed 4286 of 591805 compute units", "Program gSbePebfvPy7tRqimPoVecS2UsBvYv46ynrzWocc92s success" ], "postBalances": [ 420164575000, 23942400, 23942400, 23942400, 1169280, 1141440 ], "postTokenBalances": [], "preBalances": [ 420164580000, 23942400, 23942400, 23942400, 1169280, 1141440 ], "preTokenBalances": [], "rewards": [], "status": { "Ok": null } }, "slot": 192074782, "transaction": { "message": { "accountKeys": [ { "pubkey": "vir55LvSEGcY55ny876GycLmFCxMXTkoRg7RxDvKiw5", "signer": true, "source": "transaction", "writable": true }, { "pubkey": "8PugCXTAHLM9kfLSQWe2njE5pzAgUdpPk3Nx5zSm7BD3", "signer": false, "source": "transaction", "writable": true }, { "pubkey": "EfnLcrwxCgwALc5vXr4cwPZMVcmotZAuqmHa8afG8zJe", "signer": false, "source": "transaction", "writable": true }, { "pubkey": "6Ukmvns6Uyf3nRVj3ErDBgx7BiZRNJrLyXe1nGQ7CUHA", "signer": false, "source": "transaction", "writable": true }, { "pubkey": "SysvarC1ock11111111111111111111111111111111", "signer": false, "source": "transaction", "writable": false }, { "pubkey": "gSbePebfvPy7tRqimPoVecS2UsBvYv46ynrzWocc92s", "signer": false, "source": "transaction", "writable": false } ], "instructions": [ { "accounts": [ "vir55LvSEGcY55ny876GycLmFCxMXTkoRg7RxDvKiw5", "8PugCXTAHLM9kfLSQWe2njE5pzAgUdpPk3Nx5zSm7BD3", "SysvarC1ock11111111111111111111111111111111" ], "data": "6mJFQCt94hG4CKNYKgVcwiF4v7AWo54Dz3XinKUG6Qm1DDhhmspAST", "programId": "gSbePebfvPy7tRqimPoVecS2UsBvYv46ynrzWocc92s", "stackHeight": null }, { "accounts": [ "vir55LvSEGcY55ny876GycLmFCxMXTkoRg7RxDvKiw5", "EfnLcrwxCgwALc5vXr4cwPZMVcmotZAuqmHa8afG8zJe", "SysvarC1ock11111111111111111111111111111111" ], "data": "6mJFQCt94hG4CKNYKgVcwigC7XswHjekyj7J1dQmjsHsoqHjydqXoV", "programId": "gSbePebfvPy7tRqimPoVecS2UsBvYv46ynrzWocc92s", "stackHeight": null }, { "accounts": [ "vir55LvSEGcY55ny876GycLmFCxMXTkoRg7RxDvKiw5", "6Ukmvns6Uyf3nRVj3ErDBgx7BiZRNJrLyXe1nGQ7CUHA", "SysvarC1ock11111111111111111111111111111111" ], "data": "6mJFQCt94hG4CKNYKgVcwcojsn834cy7vrPD6ksi4ri42uvkGeVMkb", "programId": "gSbePebfvPy7tRqimPoVecS2UsBvYv46ynrzWocc92s", "stackHeight": null } ], "recentBlockhash": "sS5jHAvxaXfowtGCvg4dWc9QjzeZ1dJS5GonshyDsEq" }, "signatures": [ "2o9qCEhwKi8w7hTFQJTLwMBZPFH8qM3iNd9rprtdY6XShyrpsqkWt4Df3Zgsxv6y4nbRe4SDgU8KMvuMfs7HxVhp" ] } }, "id": 1 }我们可以通过这个代替去查看浏览器。 发送交易发送交易通过 sendTransaction 接口。这个接口里面需要对Transaction对象做编码,所以不做演示。在Javascript/rust的SDK中操作会比较直观。 除了发送请求外,还可以通过模拟请求来判断是否可能执行成功,接口为simulateTransaction。 在发送交易的时候,还可以通过getFeeForMessage来预估手续费
  12. RPC 介绍RPC 是什么? 写代码的应该都知道 RPC 是啥,但是 RPC 跟区块链是什么关系呢? 引用 Polkadot 的一个架构图: RPC 作为区块链系统与外界交互的一层接口调用。被普通用户直接使用。 但是为什么普通用户又感知不到 RPC 的存在呢?普通用户只知道钱包,拉起、确定=》 币没了。 这里是因为我们这帮程序员,帮忙将中间的过程都通过代码来串联起来了。所以 RPC 又是用户界面和区块链之间的桥梁。 Solana 提供的 RPC 分为主动请求的 HTTP 接口和消息推送的 Websocket 接口。只是单次查询一般使用 HTTP 接口,如发送交易,查询用户余额。而对于链上数据的监控则通过 Websocket 接口,如监控合约执行的日志。 HTTP 接口HTTP 接口是通过 JSON RPC 的格式对外提供服务,JSON RPC 是一种以 JSSON 作为序列化工具,HTTP 作为传输协议的 RPC 模式,其有多个版本,当前使用的是 v2 版本。 其请求格式为: Copy{ "jsonrpc": "2.0", "id": 1, "method": "getBalance", "params": [ "83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri" ] }这里最外层是一个字典,其中各个 Key 是固定的,其中 method 表示 RPC 的函数方法名。params 表示该函数的参数。 对应的请求结果为: Copy{ "jsonrpc": "2.0", "result": { }, "id": 1 }同样的,这里的几个字段也是固定的,result 表示请求的结果。id 和请求里面的 id 对应,表示的是哪个请求的结果。 在请求查询的时候,对查询的结果有三种状态选择: 'finalized' - 节点将查询由超过集群中超多数确认为达到最大封锁期的最新区块,表示集群已将此区块确认为已完成。 'confirmed' - 节点将查询由集群的超多数投票的最新区块。 'processed' - 节点将查询最新的区块。注意,该区块可能被集群跳过。 状态参数可以在"params"数组的最后,以字典的形式带入进去。 同时 Solana 也对常用的结果做了人为可读的优化。当传递"encoding":"jsonParsed"会讲结果尽量以 JSON 的方式返回。encoding 和上面的状态放在同一个位置。如: Copy{ "commitment":"processed", "encoding":"jsonParsed" }Websocket 接口Websocket 是 HTTP 为了补充长链接,而增加一个特性,概括来说就可以认为这个是一条 TCP 长链接。Solana 通过这条长连接来给客户端推送消息。 只是这里的消息的内容也是采用了 JSONRPC 的格式,如: Copy{ "jsonrpc": "2.0", "id": 1, "method": "accountSubscribe", "params": [ "CM78CPUeXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNH12", { "encoding": "jsonParsed", "commitment": "finalized" } ] }这样的消息订阅了 Account("CM78CPUeXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNH12")的变化消息。 当有变化时,也是将结果打包成一个 JSONRPC 的格式推送给客户端: Copy{ "jsonrpc": "2.0", "method": "accountNotification", "params": { "result": { "context": { "slot": 5199307 }, "value": { "data": { "program": "nonce", "parsed": { "type": "initialized", "info": { "authority": "Bbqg1M4YVVfbhEzwA9SpC9FhsaG83YMTYoR4a8oTDLX", "blockhash": "LUaQTmM7WbMRiATdMMHaRGakPtCkc2GHtH57STKXs6k", "feeCalculator": { "lamportsPerSignature": 5000 } } } }, "executable": false, "lamports": 33594, "owner": "11111111111111111111111111111111", "rentEpoch": 635, "space": 80 } }, "subscription": 23784 } }每个 Subscribe 方法,都对应的有一个 Unsubscribe 方法,当发送改方法时,服务器后续不再推送消息。
  13. 课后练习通过命令行,发行一个代币。并给自己账号mint一定数量的代币。 并通过插件钱包或者命令行的方式给其他同学空投该代币 提示: 命令行操作,查看spl-token命令的帮助文档 Copysolana config set --url https://api.devnet.solana.com示例设置环境为开发环境: 创建账号: Copysolana-keygen new --force Generating a new keypair For added security, enter a BIP39 passphrase NOTE! This passphrase improves security of the recovery seed phrase NOT the keypair file itself, which is stored as insecure plain text BIP39 Passphrase (empty for none): Wrote new keypair to /Users/you/.config/solana/id.json =========================================================================== pubkey: Czorr4y9oFvE3VdfCLVFuKDYxaNUG1iyQomR7kMZUuzi =========================================================================== Save this seed phrase and your BIP39 passphrase to recover your new keypair: tail ... despair ===========================================================================申请水龙头: Copysolana airdrop 1 Requesting airdrop of 1 SOL Signature: 3pDfybgjsP8oS4pX32f24SmTE4sTjPAsuJd43jqz6qAXu7vXBwaxmoAZQL3QquxXYxXChtiWuQWv79odj9XndG4A 1 SOL对应浏览器 3pDfybgjsP8oS4pX32f24SmTE4sTjPAsuJd43jqz6qAXu7vXBwaxmoAZQL3QquxXYxXChtiWuQWv79odj9XndG4A 创建Token: Copyspl-token create-token Creating token 7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9 under program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA Address: 7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9 Decimals: 9 Signature: 5QRdzn59ig3j3qjEazteDR2zoCLUWoCWdbFc7iQTd68esfdV9je3fE2We3Ms7NUGfBt6kapCj7oBAr1kbiTskSmzToken地址:7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9 精度: 9 交易浏览器5QRdzn59ig3j3qjEazteDR2zoCLUWoCWdbFc7iQTd68esfdV9je3fE2We3Ms7NUGfBt6kapCj7oBAr1kbiTskSmz 创建Token Account: Copyspl-token create-account 7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9 Creating account EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AK Signature: 59yBhJzC2HDkF61AhgaXcvVGiw5CjdnNpFyxvCzbqQrCjGCVKotNvCMaRQooJkxmu6ypJ9P7AZDiKxYex7pvBZKq这里实际上调用了ATA合约,并创建了ATA账号:EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AK 交易浏览器59yBhJzC2HDkF61AhgaXcvVGiw5CjdnNpFyxvCzbqQrCjGCVKotNvCMaRQooJkxmu6ypJ9P7AZDiKxYex7pvBZKq 给自己的这个Token Account发送(mint) Copyspl-token mint 7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9 100 EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AK Minting 100 tokens Token: 7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9 Recipient: EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AK Signature: 5eE21U9ukZLP7Uvck5mzBbKRcXjxEYZYxCTnJX6qoS9kdXzfhPuN8k2Ko6BBekBdP2mhLmPMHAWNJW6bqyo6mqQe交易记录 5eE21U9ukZLP7Uvck5mzBbKRcXjxEYZYxCTnJX6qoS9kdXzfhPuN8k2Ko6BBekBdP2mhLmPMHAWNJW6bqyo6mqQe 查询余额: Copyspl-token balance 7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9 100因为这里是求取ATA账号,所以只需要提供Token Mint地址即刻。 去浏览器找一个其他地址,如BBy1K96Y3bohNeiZTHuQyB53LcfZv6NWCSWqQp89TiVu,这个是个SOL地址 Copyspl-token transfer --fund-recipient 7vtXvye2ECB1T5Se8E1KebNfmV7t4VkaULDjf2v1xpA9 1 BBy1K96Y3bohNeiZTHuQyB53LcfZv6NWCSWqQp89TiVu Transfer 1 tokens Sender: EZhhUANUMKsRhRMArczio1kLc9axefTUAh5xofGX35AK Recipient: BBy1K96Y3bohNeiZTHuQyB53LcfZv6NWCSWqQp89TiVu Recipient associated token account: H1jfKknnnyfFGYPVRd4ZHwUbXLF4PbFSWSH6wMJq6EK9 Funding recipient: H1jfKknnnyfFGYPVRd4ZHwUbXLF4PbFSWSH6wMJq6EK9 Signature: 5VqeT7ctVtGdcJDvTmLzL4Pbti8PzM3mSrRpdE8GNG4ghF3svSJMkTn4AfNRQDSeYqCotEQuzDY9KLgdSJbKEjXt这里帮这个用户创建了ATA账号 H1jfKknnnyfFGYPVRd4ZHwUbXLF4PbFSWSH6wMJq6EK9 交易为5VqeT7ctVtGdcJDvTmLzL4Pbti8PzM3mSrRpdE8GNG4ghF3svSJMkTn4AfNRQDSeYqCotEQuzDY9KLgdSJbKEjXt 查询下这个账号 余额为1.
  14. 钱包使用Solana的钱包目前以Phantom、Solfare等Chrome插件为主, 类似TokenPocket、MathWallet等手机钱包为辅。甚至还有Solana Saga手机设备作为钱包。 比如打开raydium的链接钱包界面,我们可以看到这么多钱包: 五花八门,实在是太多了。这里我们抽选Phantom来举例使用。 安装这里我们以Phantom的Chrome插件为例,来演示其基本操作。当然Phantom还有Firefox、手机 等版本。其操作类似。 首先在Chrome中打开: https://chrome.google.com/webstore/detail/phantom/bfnaelmomeimhlpmgjnjophhpkkoljpa 点击"添加至Chrome" 创建/导入账号如果没有账号可以在扩展打开的时候,选择创建账号,创建账号的时候,需要记住这里的助记词,并设置一个 密码,就可以创建新账号了。 这里的助记词和我们的命令行工具的助记词是一样的。因此我们还可以选择导入账号,输入我们的已有的助记词, 并设置好密码。 同时我们还可以选择不同的网络环境,这里我们选择开发网,就可以看到我们前面领的水龙头代币了。 转账转账分为转移SOL代币和普通的SPL-Token代币,在很久以前,这里需要注意对方账户地址, 普通用户的SOL账号地址,和SPL-Token地址不一样,现在因为有了ATA账号的存在,可以直接通过 SOL账号地址进行推导,所以只需要知道对方的钱包地址也就是公钥就可以了。 在应用中打开钱包在DApp应用中,当我们点击"Connect"的时候,会弹出钱包选择界面。如前文中列出来的一样。 在这里我们选择Phantom,然后看到钱包提示我们链接应用,选择后,就可以看到账号信息了。这些 步骤和Metamask基本类似。
  15. Solana命令行接下来我们来开始体验Solana,Solana为我们提供了一套命令行工具来实现对Solana的操作。 这里注意,这个命令行工具,是除了节点外,官方提供的唯一工具。什么钱包,scan浏览器等还 都是第三方的,所以我们从这里开始。 这里建议开发工具平台使用Mac/Linux(Ubuntu),Windows不建议折腾,虽然Solana也是支持 的,下面我们以Mac作为演示平台进行讲演。 安装打开命令行,输入: Copysh -c "$(curl -sSfL https://release.solana.com/stable/install)" downloading stable installer stable commit cd1c6d0 initialized Adding export PATH="/Users/you/.local/share/solana/install/active_release/bin:$PATH" to /Users/you/.profile Close and reopen your terminal to apply the PATH changes or run the following in your existing shell: export PATH="/Users/you/.local/share/solana/install/active_release/bin:$PATH"这里需要科学上网,大家自行处理。 按照提示设置好Path,就可以验证是否安装成功了: Copy~ % solana --version solana-cli 1.14.20 (src:cd1c6d0d; feat:1879391783)这里打印出来cli的版本号。 更新到1.16.x版本 Copysolana-install init 1.16.4设置网络环境Solana的网络环境分成开发网、测试网、主网三类,开发网为Solana节点开发使用,更新频繁,测试网主要 给到DApp开发者使用,相对稳定。主网则是正式环境,里面的是真金白银。 官方RPC地址分别是: DevNet: https://api.devnet.solana.com TestNet: https://api.testnet.solana.com MainNet: https://api.mainnet-beta.solana.com 这里我们使用开发网,开发网可以申请空投测试币。 Copysolana config set --url https://api.devnet.solana.com Config File: /Users/you/.config/solana/cli/config.yml RPC URL: https://api.devnet.solana.com WebSocket URL: wss://api.devnet.solana.com/ (computed) Keypair Path: /Users/you/.config/solana/id.json Commitment: confirmed创建账号执行: Copysolana-keygen new --force Generating a new keypair For added security, enter a BIP39 passphrase NOTE! This passphrase improves security of the recovery seed phrase NOT the keypair file itself, which is stored as insecure plain text BIP39 Passphrase (empty for none): Wrote new keypair to /Users/you/.config/solana/id.json ======================================================================== pubkey: 5pWae6RxD3zrYzBmPTMYo1LZ5vef3vfWH6iV3s8n6ZRG ======================================================================== Save this seed phrase and your BIP39 passphrase to recover your new keypair: pistol note gym mesh public endless salt maximum ... ========================================================================这里设置好密码后,提示keypair被加密存在存在"/Users/you/.config/solana/id.json"。 同时其对应的BIP39的助记词为: Copypistol note gym mesh public endless salt maximum ...对应的地址:5pWae6RxD3zrYzBmPTMYo1LZ5vef3vfWH6iV3s8n6ZRG 这里助记词要记住,后续使用钱包的时候,可以通过助记词来恢复账号。 通过如下命令可以查看当前账号的地址,也就是上面的Keypair文件的中的公钥: Copysolana-keygen pubkey 5pWae6RxD3zrYzBmPTMYo1LZ5vef3vfWH6iV3s8n6ZRG申请水龙头只有开发网和测试网可以申请水龙头币,这里可以通过命令行: Copysolana airdrop 1 Requesting airdrop of 1 SOL Signature: 4xYKfGjWcLir8F6puSzVWafbqYhjSyESNKygPygia6RgomSJACy5MhoKXhiePtz6VQ5W8DxYF5baeB4Cf9oKnkqy 1 SOL提示申请1个SOL成功。通过命令 Copysolana balance 1 SOL可以查看当前账号的余额。当前账号也就是"/Users/you/.config/solana/id.json"中存储的keypair对应的账号。 转账这里通过命令行给另一个账号转账: Copysolana transfer --allow-unfunded-recipient CZmVK1DymrSVWHiQCGXx6VG5zgHVrh5J1P514jHKRDxA 0.01 Signature: 3wDKwR1GFiKoUzmNJSdTYaoKp5n5fYxNCD712V9Vpj15M6UyK2A2Gtvb8GaiaGHoA8GJki8rqTuCuHnsWiGej7rV如果这个账号之前不存在,需要使用--allow-unfunded-recipient来进行创建。这里输出的交易hash,我们可以 在浏览器中看到结果。 需要注意的是,这里要把环境选择为Testnet环境,才能看到我们的这个执行结果。
  16. SPL 代币在以太坊中,普通代币被一个叫ERC20的提案定了规范,可以认为普通代币合约统一叫做ERC20代币。 那么Solana世界里的ERC20代币是什么呢?答案就是SPL代币。 SPL Token是 " Solana Program Library"中的一个组成部分,叫做"Token Program",简称为SPL Token。 所有的代币都有这个合约来管理,该合约代码在 https://github.com/solana-labs/solana-program-library/tree/master/token 代币信息不同于以太坊中,一个代币就是一个合约。 SPL Token中,一个代币,仅仅是一个归Token合约管理的普通的Account对象,这个对象里面的二进制数据定义了 这个代币的基本属性。其结构为: Copypub struct Mint { /// Optional authority used to mint new tokens. The mint authority may only be provided during /// mint creation. If no mint authority is present then the mint has a fixed supply and no /// further tokens may be minted. pub mint_authority: COption<Pubkey>, /// Total supply of tokens. pub supply: u64, /// Number of base 10 digits to the right of the decimal place. pub decimals: u8, /// Is `true` if this structure has been initialized pub is_initialized: bool, /// Optional authority to freeze token accounts. pub freeze_authority: COption<Pubkey>, }相对有意义的就是supply表示总共的供应量,decimals表示代币的精度信息。 SPL Token Account那么每个用户的拥有的代币数量信息存在哪里呢? 这个合约又定义了一个账号结构,来表示某个地址含有某个代币的数量。 Copypub struct Account { /// The mint associated with this account pub mint: Pubkey, /// The owner of this account. pub owner: Pubkey, /// The amount of tokens this account holds. pub amount: u64, /// If `delegate` is `Some` then `delegated_amount` represents /// the amount authorized by the delegate pub delegate: COption<Pubkey>, /// The account's state pub state: AccountState, /// If is_native.is_some, this is a native token, and the value logs the rent-exempt reserve. An /// Account is required to be rent-exempt, so the value is used by the Processor to ensure that /// wrapped SOL accounts do not drop below this threshold. pub is_native: COption<u64>, /// The amount delegated pub delegated_amount: u64, /// Optional authority to close the account. pub close_authority: COption<Pubkey>, }这里owner表示谁的代币,amount表示代币的数量。 Account关系所以整体结构是这样的: 这两个结构体都是SPL Token Program管理的Account对象,其自身所携带的数据,分别为代币信息,和 存储哪个代币的信息。 这样当需要进行代币的交易时,只需要相应用户的相应代币账号里面的amount即可。
  17. Solana核心概念Account在Solana中,"Everythin is an Account" 类似Linux世界里面把所有的资源都抽象成"文件"一样。 Solana作为一个分布式区块链系统,所有的信息都存储在Account对象中,如合约(Solana叫Onchain Program), 账号信息,合约中存储的内容等都是存储在一个个Account对象中。 Account的定义如下: Copypub struct Account { /// lamports in the account pub lamports: u64, /// data held in this account #[serde(with = "serde_bytes")] pub data: Vec<u8>, /// the program that owns this account. If executable, the program that loads this account. pub owner: Pubkey, /// this account's data contains a loaded program (and is now read-only) pub executable: bool, /// the epoch at which this account will next owe rent pub rent_epoch: Epoch, }其中的lamports表示账号余额,data表示存储的内容,owner表示这个Account可以被谁来操作,类似文件所有者。 如果是合约账号,这里data的内容就是合约编译后的代码,同时executable为true。 账号和签名Solana的签名系统使用的是 Ed25519 ,说人话就是: Ed25519是一种计算快,安全性高,且生成的签名内容小的一种不对称加密算法。新一代公链几乎都支持这个算法。 所以Solana的,我们用户理解的账号,就是一串Ed25519的私钥,各种钱包里面的助记词,会被转换成随机数种子, 再用随机数种子来生成一个私钥,所以助记词最终也是换算成私钥。所以用户账号的本质就是私钥,而用户账号的地址 则是这私钥对应的公钥,优于公钥是二进制的,为了可读性,将其进行Base58编码后的值,就是这个账号的地址。 如:HawRVHh7t4d3H3bitWHFt25WhhoDmbJMCfWdESQQoYEy 把这里的公钥和私钥放一起,就是所谓的Keypair,或者叫公私钥对。假设这里把私钥进行加密,并由用户来设置密码, 公钥作为这个私钥的索引。就实现了一个简单的钱包系统了。 通过用户选择的公钥,加上密码,得到对应的私钥,再用私钥去操作的他的账号 交易交易就是链外数据和链上数据产生的一次交互。比如发起一笔转账,在StepN里面发起一次Claim动作。 交易是对多个交易指令的打包,所以起内容主要就是各个交易指令,以及相应指令对应的发起人和签名。 Transaction的定义为: Copypub struct Message { /// The message header, identifying signed and read-only `account_keys`. /// Header values only describe static `account_keys`, they do not describe /// any additional account keys loaded via address table lookups. pub header: MessageHeader, /// List of accounts loaded by this transaction. #[serde(with = "short_vec")] pub account_keys: Vec<Pubkey>, /// The blockhash of a recent block. pub recent_blockhash: Hash, /// Instructions that invoke a designated program, are executed in sequence, /// and committed in one atomic transaction if all succeed. /// /// # Notes /// /// Program indexes must index into the list of message `account_keys` because /// program id's cannot be dynamically loaded from a lookup table. /// /// Account indexes must index into the list of addresses /// constructed from the concatenation of three key lists: /// 1) message `account_keys` /// 2) ordered list of keys loaded from `writable` lookup table indexes /// 3) ordered list of keys loaded from `readable` lookup table indexes #[serde(with = "short_vec")] pub instructions: Vec<CompiledInstruction>, /// List of address table lookups used to load additional accounts /// for this transaction. #[serde(with = "short_vec")] pub address_table_lookups: Vec<MessageAddressTableLookup>, } pub enum VersionedMessage { Legacy(LegacyMessage), V0(v0::Message), } pub struct VersionedTransaction { /// List of signatures #[serde(with = "short_vec")] pub signatures: Vec<Signature>, /// Message to sign. pub message: VersionedMessage, }从中可以简单理解为,交易就是一连串的交易指令,以及需要签名的指令的签名内容。 交易指令上面说到的交易指令又是什么呢?先来看下定义: Copypub struct CompiledInstruction { /// Index into the transaction keys array indicating the program account that executes this instruction. pub program_id_index: u8, /// Ordered indices into the transaction keys array indicating which accounts to pass to the program. #[serde(with = "short_vec")] pub accounts: Vec<u8>, /// The program input data. #[serde(with = "short_vec")] pub data: Vec<u8>, }从这些成员变量名就可以猜到。交易指令就是 执行哪个合约(program_id_index),输入为数据data,执行过程 中需要用到哪些Account: accounts 类似函数调用一样,program_id_index是函数名,因为合约都是用地址标识的,所以这里指在accounts数组中 的第几个地址。传入的参数包含两部分,二进制数据data和需要使用到的Account资源:accounts。 合约合约分为两类,一类是普通合约一类是系统合约,前者在Solana中称为"On Chain Program" 后者称为"Native Program" 其实本质都是类似其他公链上所说的合约。 系统合约系统合约是由节点在部署的时候生成的,普通用户无法更新,他们像普通合约一样,可以被其他合约或者RPC进行调用 系统合约有 System Program: 创建账号,转账等作用 BPF Loader Program: 部署和更新合约 Vote program: 创建并管理用户POS代理投票的状态和奖励 ... 普通合约一般我们说的合约都是普通合约,或者叫 "On Chain Program"。普通合约是由用户开发并部署,Solana官方也有 一些官方开发的合约,如Token、ATA账号等合约。 当用户通过"BPF Loader Program"部署一个新合约的时候,新合约Account中的被标记为true,表示他是一个可以 被执行的合约账号。不同于有些公链,Solana上的合约是可以被更新的,也可以被销毁。并且当销毁的时候,用于存储 代码的账号所消耗的资源也会归还给部署者。 合约与Account在上面的Account介绍中,我们有个owner的成员,这个就表示这个Account是被哪个合约管理的,或者说哪个 合约可以对这个Account进行读写,类似Linux操作系统中,文件属于哪个用户。 比如一般合约,他的Owner都是BPF Loader: 而存放我们代币余额的内容的ower都是Token合约: 对应的代币为: 租约Solana的资金模型中,每个 Solana 账户在区块链上存储数据的费用称为“租金”。 这种基于时间和空间的费用来保持账户及其数据在区块链上的活动为节点提供相应的收入。 所有 Solana 账户(以及计划)都需要保持足够高的 LAMPORT 余额,才能免除租金并保留在 Solana 区块链上。 当帐户不再有足够的 LAMPORTS 来支付租金时,它将通过称为垃圾收集的过程从网络中删除。 注意:租金与交易费用不同。 支付租金(或保存在账户中)以将数据存储在 Solana 区块链上。 而交易费用是为了处理网络上的指令而支付的。 租金率Solana 租金率是在网络范围内设置的,主要基于每年每字节设置的 LAMPORTS。 目前,租金率为静态金额并存储在 Rent 系统变量中。 免租保持最低 LAMPORT 余额超过 2 年租金的账户被视为“免租金”,不会产生租金。 每次账户余额减少时,都会检查该账户是否仍免租金。 导致账户余额低于租金豁免阈值的交易将会失败。 垃圾收集未保持租金豁免状态或余额不足以支付租金的帐户将通过称为垃圾收集的过程从网络中删除。 完成此过程是为了帮助减少不再使用/维护的数据的网络范围存储。 关于租约的提案
  18. Solana介绍一句话概括 一张图表示 曾经也是辉煌夺目 Solana历史2017年11月,Anatoly Yakovenko发表了一篇白皮书,介绍了“Proof of History”这一技术,用于在不信任彼此的计算机之间进行时间同步。根据Anatoly在高通、Mesosphere和Dropbox设计分布式系统的经验,他知道可靠的时钟可以使网络同步变得非常简单。当同步变得简单时,结果的网络可以非常快速,仅受网络带宽的限制。 Anatoly注意到,没有时钟的区块链系统(如比特币和以太坊)在全球范围内的交易速度在15次每秒时遇到困难,而世界中心化支付系统(如Visa)则需要峰值65000次每秒。 没有时钟,很明显他们永远无法成为全球支付系统或全球超级计算机。当Anatoly解决了计算机之间不信任时间一致性的问题时,他知道他拥有将40年分布式系统研究带给区块链世界的关键。由此产生的集群不仅仅是10倍、100倍或1000倍,而是立即实现了出厂时是一万倍的速度! Anatoly的实施开始是在一个私人代码库中,使用C编程语言进行实现。曾在Qualcomm Incorporated与Anatoly一起合作的Greg Fitzgerald鼓励他以Rust编程语言重新实现该项目。Greg曾在LLVM编译器基础设施上工作,该基础设施是Clang C/C++编译器和Rust编译器的基础。Greg声称该语言的安全保证将提高软件的生产率,并且其无垃圾回收器将使程序能够像使用C编写的程序一样运行。Anatoly尝试了一下,仅用了两个星期,就将他的整个代码库迁移到了Rust上。成功了。计划将全球的交易编织在一个可伸缩的区块链上,Anatoly将该项目命名为Loom。 2018年2月13日,Greg开始为Anatoly的白皮书创建开源实现的原型。该项目在Loom协议组织的GitHub上发布,命名为Silk。2月28日,Greg发布了首个版本,演示了超过10,000个签名交易可以在半秒内验证和处理。不久之后,另一位曾在高通工作的同事Stephen Akridge演示了通过将签名验证转移至图形处理器可以大大提高吞吐量。Anatoly邀请Greg、Stephen和其他三个人共同创办了一家名为Loom的公司。 与此同时,基于以太坊的项目Loom Network涌现出来,许多人对它们是否是同一个项目感到困惑。Loom团队决定进行重新品牌推广。他们选择名为Solana的名字,以致敬他们在工作于高通期间住和冲浪了三年的圣地亚哥北部的一个小海滩城镇Solana Beach。 2018年3月28日,团队创建了Solana GitHub组织,并将Greg的原型命名为Solana。 2018年6月,团队将技术扩展到云网络上运行,并于7月19日发布了一个50个节点的许可,且公开的测试网络,能够始终支持每秒25万个交易的突发。在稍后于12月的v0.10 Pillbox版本中,团队发布了一个以千兆位网络运行150个节点的许可测试网络,演示了平均每秒处理20万个交易和突发500万个交易的吸收测试。该项目还扩展到支持使用C编程语言编写的链上程序,并在称为SBF的安全执行环境中并行运行。 Solana开发流程Solana网络是一个庞大的全球计算机,任何人都可以支付费用来存储和执行代码。部署的代码被称为程序,在其他区块链上通常被称为智能合约。要与程序交互,您需要从客户端在区块链上发送一笔交易。这是一个高层次的表示。需要注意的是,这只是为了简单易懂而对Solana网络的过度简化。 Solana开发者工作流程是程序-客户(program-client)模型。程序开发的第一个工作流程允许您直接创建和部署自定义的Rust、C和C++程序到区块链。一旦这些程序部署完成,任何知道如何与它们通信的人都可以使用它们。您可以使用任何可用的客户端SDK(或CLI)编写dApps来与这些程序通信,所有这些SDK都在底层使用JSON RPC API。 客户端开发是第二个工作流,您可以在这里编写与部署的程序通信的dApp。您的应用程序可以通过客户端SDK向这些程序提交交易指令,以创建各种应用程序,如钱包、交易所等。最常用的应用程序是浏览器扩展钱包和Web应用程序,但您也可以构建移动、桌面应用程序或任何能够与JSON RPC API通信的应用程序。 这两个部分共同工作,创建了一个由dApp和程序组成的网络,它们可以相互通信以更新状态并查询区块链。 也就是所谓的前端开发和合约开发。后续我们的课程也是围绕着两个方向来展开
    2025最新版中英双语云挖矿授权质押理财生息源码,完整支持USDT/BTC/ETH质押生息、自动结算、团队分佣、KYC认证及OTC法币出入金。基于ThinkPHP+MySQL高并发架构,附赠安卓/iOS双端APP与详细搭建教程,适合交易所、钱包、资管平台快速上线。 一、产品亮点 真·完整源码:后端PHP7.4、前端Vue3、合约Solidity全部开源,无加密无后门。 中英双语:一键切换,后台可扩展更多语言包,海外流量轻松承接。 金融级安全: • 智能合约已通过CertiK安全审计 • 防重入、防闪电贷攻击、多签+Timelock双重保险 高收益模型: • 灵活质押周期(7/30/90/180天) • 日利率、年化收益后台可调,支持阶梯利率 • 自动复投+推荐返佣最高3级,裂变式增长 全终端覆盖: • H5响应式,完美适配手机、平板 • 安卓原生+Flutter壳,上架Google Play无忧 • iOS免签封装,无需企业签名即可分发 支付&合规: • 对接USDT-TRC20/BSC-BEP20自动到账 • KYC/AML实名认证API一键接入 • 后台审计日志满足各国监管上报需求 二、技术架构 • 后端:ThinkPHP8 + MySQL8 + Redis6 队列 • 前端:Vue3 + Vite + Element-Plus • 合约:Solidity 0.8.x,支持ETH/BSC/Polygon多链部署 • 环境:PHP7.4+、MySQL5.7+、Nginx1.20+、Redis6+ • 并发:实测2核4G服务器可稳定支撑5,000人在线质押 三、核心功能清单 表格 复制 模块 功能点 备注 用户端 注册/登录、谷歌验证、KYC 支持邮箱+手机双因子 质押 灵活质押、复投、提前赎回 提前赎回手续费后台可调 收益 实时收益曲线、每日结算推送 可配置站内信/邮件/短信 团队 三级分佣、排行榜、裂变海报 支持海报二维码自动生 OTC USDT买卖、订单申诉、商户认证 自带商户信誉评分系统 运营 轮播/公告/工单、风控黑名单 后台一键封禁与解冻 财务 提现审核、手续费、资金池 冷热钱包分离+多签 四、安装教程(10分钟上线) 上传源码到服务器,设置运行目录/public 导入database.sql,修改config/database.php 配置.env中的区块链节点、USDT合约地址、邮件SMTP 设置Crontab定时任务,每1分钟执行一次收益结算 开启HTTPS、配置防火墙,完成! 五、常见问题FAQ Q1:源码是否加密? A:100%开源,除第三方类库外无加密文件,可放心二开。
    30.00 USD
  19. WEB3论坛回复到 admin的主题在 咖啡室
    去中心化论坛
    区块链养成游戏、NFT宠物挖矿、EMK代币、BSC链游、边玩边赚、GameFi源码 一、项目亮点(SEO一句话总结) Doggy Farm 是一款部署在 BNB Smart Chain(BSC)上的 NFT 养成链游,330 元即可铸造专属“戏猪”NFT,静态日收益 5%–15%,动态推广最高返佣 13%,支持 USDT、EMK 双币闪兑,所有收益由智能合约自动结算,链上透明可查。 二、如何开始(3 步上车) 注册钱包:MetaMask、TokenPocket 均可,网络切换至 BSC。 铸造 NFT:智能合约地址 0x9F…A1c3,输入 330 USDT 立即获得 1 只「戏猪」NFT。 次日收菜:NFT 自动产出 EMK 代币,可随时提取或复投。 三、4 级 NFT 宠物收益对照表(SEO表格) 表格 复制 稀有度 区块价值(USDT) 抢购时段(UTC+8) 预约/抢购燃料费 合约周期 日收益率 日挖EMK 普通 50–100 19:00–19:30 5 / 10 5 天 15% 4 卓越 100–300 19:30–20:00 4 / 8 1 天 5% 5 勇者 200–500 20:10–20:20 15 / 30 7 天 14% 8 至尊 200–500 20:40–21:00 20 / 40 10 天 15% 9 四、玩法攻略 静态玩家 • 每日 00:00 系统快照 NFT 权重,按上表比例空投 EMK 至钱包,无需任何操作。 动态玩家 • 直推 1 代返佣 8%,2 代 3%,3 代 2%,返佣实时到账,可复投升级宠物。 • 成为“渔场主”后,团队业绩 ≥ 500 USDT 起,享受全网手续费 5% 加权分红。 偷猪彩蛋 • 每日 12:00–13:00 开放好友互动,可“偷取”好友未收取的 EMK 收益,单次上限 5%。 五、智能合约规则(链上透明) • NFT 到期强制挂单:合约自动将 NFT 挂至市场,其他玩家可预约领养。 • 失约惩罚:预约未领养,燃料费不退,自动回流奖池。 • 权重分配:合约依据玩家活跃度、推广度、燃料费等多维权重随机匹配领养顺序。 • 结算逻辑:领养成功后 2 小时内链上转账 → 上传凭证 → 双方确认,超时系统自动放行。 六、经济模型 • 代币:EMK(BEP-20)总量 1 亿,其中 60% 用于游戏挖矿,10% 空投给早期 NFT 持有者,30% 线性解锁至流动性池。 • 通缩:每笔 NFT 交易 5% 销毁,2% 回流奖池,3% 用于回购销毁。 七、风险提示 本项目基于 BSC 智能合约运行,所有交互均在链上完成,请妥善保管私钥,理性投入。本文仅供技术分享,不构成投资建议。
    100.00 USD
    区块链代币销售平台、加密货币拍卖系统源码、Laravel ICO平台、DeFi跨链众筹、KYC数字资产交易 一、项目亮点(SEO精选长尾词布局) 合规先行 • 内置全球KYC/AML模块,对接Jumio、Onfido API,3分钟完成身份核验,满足美国MSB、欧盟MiCA及香港VASP最新监管指引。 多链支付网关 • 原生支持BTC、ETH、BNB、SOL、TRON等主链及150+代币;集成CoinPayments、NOWpayments、Stripe Crypto,法币与加密货币双向闪兑。 拍卖&众筹双引擎 • 英式、荷式、密封拍卖三种模式,支持阶段轮次众筹+实时竞价,可设定白名单、硬顶、释放规则,适配GameFi、RWA等各类资产。 安全架构 • Laravel 8.x + PostgreSQL 14主从读写分离;冷热钱包分离,98%资产多签冷存;Google reCAPTCHA v3、2FA、短信&邮件双通道验证。 开发者友好 • RESTful & WebSocket API 文档自动生成(Swagger),支持Docker一键部署;前端采用Vue3 + Vite,SSR渲染提升SEO。 二、系统核心功能(附源码目录结构) 用户端 • 邮箱/手机注册、KYC等级、谷歌OTP、登录日志、设备管理。 • 资产总览:现货、锁仓、拍卖保证金、收益分成实时统计。 • 拍卖大厅:实时竞价曲线、倒计时、自动加价机器人。 管理端 • 项目方审核、代币上架、轮次配置、白名单空投。 • 报告中心:链上充值提现、拍卖成交、用户KYC漏斗、Google Analytics 4 事件。 • 工单系统:支持SLA、智能分派、邮件&Telegram通知。 运营工具 • 页面构建器:拖拽生成Landing Page,内置SEO元标签、OG协议、结构化数据。 • 邮件&短信营销:Mailgun、AWS SES、Twilio 多通道模板化推送。 • 推荐返佣:三级分佣、链上透明结算、实时排行榜。 三、技术栈与二次开发指南 • 后端:Laravel 8.x + PHP8.1、PostgreSQL 14、Redis 7、RabbitMQ、Laravel Horizon队列监控。 • 前端:Vue3、Pinia、TypeScript、TailwindCSS、SSR渲染(Nuxt3)。 • 区块链:Web3.js、Ethers.js、Truffle 套件;已写好ETH、BSC、Polygon、TRON四条链的部署脚本。 • 容器化:Docker-Compose、GitHub Actions CI/CD、Let's Encrypt SSL自动续期。 四、快速开始(5分钟搭建) 内置安装教程 # 1. neizhi anzhhuang 访问 https://your-domain 即可体验完整功能。 五、典型部署场景 • IDO Launchpad:一键开启项目众筹,支持白名单+拍卖混合模式。 • NFT盲盒:将拍卖引擎改造为随机开盒,链上随机数+VRF可验证。 • RWA代币化:与房地产、碳信用资产对接,收益权代币化后二级拍卖。
    100.00 USD
  20. 萨达阿萨德阿萨德

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.