move 学习

2025-06-17

1. 区块链基础概念

1. 区块链平台:区块链平台是一个分布式数据库,,它由许多计算机节点组成。这些计算机节点共同维护着一个共享的账本,称为区块链。(比如sui)
2. 智能合约: 运行在区块链平台上的计算机程序,智能合约主要执行3件事:创建资产,管理资产,权限控制
3. move语言:智能合约编程语言,可以类比于c语言这种计算机编程语言

4. 区块链资产(Blockchain  Asset):区块链网络上记录和存储的数字化资产,比如加密货币(比特币),稳定币(加密货币的一种,但和法定货币直接挂钩,如usdt),NFT(数字资产,艺术品),Coin代币(代表各种东西,如公司股票)
5. 区块链账户(Blockchain Account):是在区块链网络中用于存储和管理用户资产和交易记录的数字账户。每个参与特定区块链网络的用户都可以拥有一个或多个区块链账户。(实际上就是公私钥对,公钥做地址,私钥做身份认证)
6. 资产所有权(Ownership):资产所有权是指对资产的占有、使用、收益和处分的权利。
资产所有权可以分为以下几种类型:
独有权:私有权是指个人资产的专有所有权。
共有权:共有权是指两个人或两个人以上共同对资产的所有权。
7. 区块链钱包(Blockchain Wallet):是一种用于存储、管理和交换加密货币的数字钱包。它是在区块链技术基础上开发的工具,允许用户安全地保存其加密货币资产(包括生成区块链账号),并进行交易和管理。
区块链钱包的类型有很多,包括:
热钱包:热钱包是指连接互联网的区块链钱包。热钱包比较方便使用,但安全性较低。
冷钱包:冷钱包是指不连接互联网的区块链钱包。冷钱包比较安全,但使用起来不太方便。
硬件钱包:硬件钱包是一种物理设备,它可以用来存储和管理区块链资产。硬件钱包非常安全,但价格比较昂贵。
8. 区块链浏览器(Blockchain Explorer):一种用于浏览和查询区块链上数据的工具。它提供了一种用户友好的界面,使用户能够浏览区块链上的交易、区块、地址和其他相关信息。

2. 环境搭建

2.1 sui(区块链平台)

sui可以通过brew快速安装。

# 下载安装脚本
git clone --depth=1 https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/install.git brew-install

# 执行安装脚本
./brew-install/install.sh

# 将brew添加到环境变量,路径为/home/your_user/.bashrc
echo >> /home/wsxk/.bashrc
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> /home/wsxk/.bashrc
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"

# 用brew安装sui
brew install sui

2.2 Sui Wallet(区块链钱包)

其实可配可不配。
Sui的区块链钱包有多种,基本功能是一致的(存储、管理资产),不同的区块链钱包提供了不同的功能。
这里我安装了Sui Wallet(slush)
在chrome浏览器上安装插件即可。
https://chrome.google.com/webstore/detail/opcgpfmipidbgpenhmajoajpbobppdil
安装步骤比较简单,无需多言

2.3 suiscan(区块链浏览器)

区块链浏览器用于查看区块链,这里提供两个区块链浏览器:
https://suiscan.xyz/
https://suivision.xyz/

2.4 faucet(测试网sui)

sui(区块链平台)中有很多张区块链网络,当然作为学习用途,我们肯定不能直接花钱买币。所以为了能白嫖币供我们使用,我们需要连接测试网,俗称水龙头https://faucet.sui.io/
获取sui token的办法https://docs.sui.io/guides/developer/getting-started/get-coins

2.5 VScode move插件

参考https://docs.sui.io/references/ide/move
要想写move语言,ide必不可少:我使用VScode来编写,但是需要安装相应插件,安装步骤如下:

sudo apt-get install curl git-all cmake gcc libssl-dev pkg-config libclang-dev libpq-dev build-essential
sudo snap install rustup --classic
rustup default stable
cargo install --git https://github.com/MystenLabs/sui.git sui-move-lsp

随后在VSCODE插件发布市场里搜索mysten.move并安装即可
安装完成后,在terminal执行命令sui move new hello_move,并抄抄代码:

module hello_move::hello ;

use std::ascii::{String, string};
use sui::object::{Self, UID};
use sui::transfer::transfer;
use sui::tx_context::{TxContext, sender};

public struct Hello has key {
    id: UID,
    say: String
}

fun init(ctx: &mut TxContext) {
    let hello_move = Hello {
        id: object::new(ctx),
        say: string(b"move"),
    };
    transfer(hello_move, sender(ctx));
}

2.6 发布项目

使用/home/wsxk/.sui/sui_config/client.yaml文件配置区块链
配置完成后,它会自动生成新的地址和私钥。
接下来使用sui client faucet命令获取sui tokens
接着使用sui client gas查看结果:
接着再运行一次sui client publish

发布成功后,可以到区块链浏览器上查看详情:
其中,Transaction Effects中的Digest字段的值,可以让我们在区块链浏览器中找到我们发布事件
另外,若想看到sui替你生成的钱包地址和私钥,可以查看~/.sui/sui_config/client.yaml~/.sui/sui_config/sui.keystore

2.7 cli调用函数的方法

sui client publish //发布上链
sui client call --package package_id --module module_name --function func_name --args .... 
//如果参数中需要用到地址,需要记住参数的object id

2.8 本地调试

上节讲的是将所写代码发布到测试网testnet然后尝试调用某个公开的function,在实际编写move代码时,为了能够更有效的查看代码结果,发布在testnet上还是不方便。
目前比较好用的还是单元测试,在项目的代码中添加:

#[test]
public fun test_a_greater_than_b() {
  debug::print(&string::utf8(b"test start!"));
}

然后terminal中执行sui move test即可

2.9 远程调用合约

https://learnblockchain.cn/article/7623里面写的比较详细

2.10 查看链上数据

通常情况下,在区块链浏览器上是可以搜到对象的内容的,但是里面的dynamic-field内容无法查看,这时候可以在区块链浏览器找到有dynamic-field的对象的id,如:
接着调用sui client dynamic-field 0x7dd04814002d50a2f2a585aa2bba04967d0ea307e2e199926994c54516f67602即可看到
接下来,要查看里面具体的值,就需要调用sui client object 0x7dd04814002d50a2f2a585aa2bba04967d0ea307e2e199926994c54516f67602查看具体内容:

3. Move语言基本概念

其实只要了解Move语言中比较关键的部分,其余部分和诸如c语言等编程语言是类似的。

3.1 package、module

package是指同一个合约地址包含的全部代码的集合,由很多模块组成,比如一个项目sui move new hello_move
module就是package中的其中一个模块,比如hello_move项目中的hello_move.move文件

3.2 方法的访问权限控制

方法是module中的函数,类比与c的函数
跟c语言不同的是,方法前面可以添加关键字来确定这个方法的访问权限,即这个方法能被谁调用
一般访问权限控制由如下几种模式:

1. fun call():                 表示只有module内可见,只能在module内被调用
2. public(package) fun call(): 表示只对当前package内可见,可以在package内被调用
3. public fun call():          表示对任意package可见,其他package都可以调用,包括 sui client call
4. entry fun call():           表示能被链下其他事务调用(即dapp(RPC)调用)
5. public entry fun call():    表示任意package,链下其他事务都能调用。

3.3 object和资产和资产所有权

objectstruct演变而来:

//寻常struct
public struct NAME {
    FIELD1: TYPE1,
    FIELD2: TYPE2,
    ...
}
//每个struct最多有4个能力
//key:在全局存储操作中值可以被用为key键
//store:值可以在全局存储中被存储
//copy:值可以被复制
//drop:作用域范围结束后可以被丢弃

//object,加个 has key,且结构体的第一个成员变量一定是UID类型,就会变成object(对象),move中这种object就代表一个资产。
public  struct ColorObject has key {
        id: UID,
        red: u8,
        green: u8,
        blue: u8,
}

资产是有所有权的(你也不想你的钱也是别人的钱吧)
move语言中,所有权分为独享所有权和共享所有权,有如下表现形式:

1. 对于只有key属性的独享所有权对象,要进行所有权转移,需要使用transfer方法
2. 对于有key+store属性的独享所有权对象,要进行所有权转移,需要使用public_transfer方法
3. 对于只有key的共享所有权对象,在使用时必须进行share_object方法来设置对象
4. 对于有key+store的共享所有权对象,在使用时必须进行public_share_object来设置对象

3.4 函数 参数传递和删除object

需要注意的是,函数在使用这些所有权对象时,访问权限会发生转移:

fun f(consume: T, write: &mut T, read: &T)
T: transfer, delete, write, read  //这种场景下相当于访问权限从父函数传递到了f函数,父函数之后不能再使用,除非f函数又将T返回
&mut T: write, read //父函数还是拥有先前权限
&T: read//父函数还是拥有先前权限

函数的访问权限和对象所有权是有区别的,需要注意。

3.5 Capibility设计模式

本质上是一种权限的管控,如下:

// Type that marks the capability to create, update, and delete transcripts
public struct TeacherCap has key {
  id: UID
}

//只有拥有TeacherCap能力的调用者,才能完成调用
public entry fun create_wrappable_transcript_object(_: &TeacherCap, history: u8, math: u8, literature: u8, ctx: &mut TxContext) {
  let wrappableTranscript = WrappableTranscript {
      id: object::new(ctx),
      history,
      math,
      literature,
  };
  transfer::transfer(wrappableTranscript, tx_context::sender(ctx))
}

3.6 witness设计模式

witness是一种设计模式,用于证明有关的一个资源或类型 A,在短暂的 witness 资源被消耗后只能启动一次。witness 资源在使用后必须立即被消耗或丢弃,确保它不能被重复使用以创建 A 的多个实例。
通常会在init阶段使用。(通常只用一次,也叫OTW(one-time witness))

/// Module that defines a generic type `Guardian<T>` which can only be
/// instantiated with a witness.
module witness::peace {
  use sui::object::{Self, UID};
  use sui::transfer;
  use sui::tx_context::{Self, TxContext};

  /// Phantom parameter T can only be initialized in the `create_guardian`
  /// function. But the types passed here must have `drop`.
public struct Guardian<phantom T: drop> has key, store {
      id: UID
  }

  /// This type is the witness resource and is intended to be used only once.
  struct PEACE has drop {}//这就是witness变量,witness必须有drop能力。

  /// The first argument of this function is an actual instance of the
  /// type T with `drop` ability. It is dropped as soon as received.
  public fun create_guardian<T: drop>( //witness虽然没有被用到变量当中,但是却表示了这个结构体是唯一的。
      _witness: T, ctx: &mut TxContext
  ): Guardian<T> {
      Guardian { id: object::new(ctx) }
  }

  /// Module initializer is the best way to ensure that the
  /// code is called only once. With `Witness` pattern it is
  /// often the best practice.
  fun init(witness: PEACE, ctx: &mut TxContext) {
      transfer::transfer(
          create_guardian(witness, ctx),
          tx_context::sender(ctx)
      )
  }
}

3.7 coin

coin是move语言中代币非常重要的结构体,用于发行你自己的货币。其定义如下:

public struct Coin<phantom T> has key, store {
        id: UID,
        balance: Balance<T>
    }
public struct Balance<phantom T> has store {
    value: u64
}

coin类型的代币,通过create_currency来创建,其定义如下:

    public fun create_currency<T: drop>(
        witness: T,
        decimals: u8,
        symbol: vector<u8>,
        name: vector<u8>,
        description: vector<u8>,
        icon_url: Option<Url>,
        ctx: &mut TxContext
    ): (TreasuryCap<T>, CoinMetadata<T>) {
        // Make sure there's only one instance of the type T, OTW模式
        assert!(sui::types::is_one_time_witness(&witness), EBadWitness);

        // Emit Currency metadata as an event.
        event::emit(CurrencyCreated<T> {
            decimals
        });

        (
            TreasuryCap {
                id: object::new(ctx),
                total_supply: balance::create_supply(witness)
            },
            CoinMetadata {
                id: object::new(ctx),
                decimals,
                name: string::utf8(name),
                symbol: ascii::string(symbol),
                description: string::utf8(description),
                icon_url
            }
        )
    }

它返回两个对象,一个是TreasuryCap资源,另一个是CoinMetadata资源
TreasuryCap其实就是存放这个代币的发行总额,所以只能有一个单例来记录。

    /// Capability allowing the bearer to mint and burn
    /// coins of type `T`. Transferable
public struct TreasuryCap<phantom T> has key, store {
            id: UID,
            total_supply: Supply<T>
        }
/// A Supply of T. Used for minting and burning.
    /// Wrapped into a `TreasuryCap` in the `Coin` module.
struct Supply<phantom T> has store {
        value: u64
    }

CoinMetadata用于记录这个代币的一些基本信息,创建出来后通常都会使用freeze_object来冻结,使其只读。

3.8 同类集合和异构结合

同类集合其实指的是集合中存放的类型都是一样的,异构结合指的是集合中可以存放的类型可以不一样。
move语言中的三个集合类型,分别为vector(同类集合),table(同类集合),bag(异构结合)

3.8.1 vector

vector本质上和c++中的vector是一样的:

module collection::vector {

    use std::vector;

    public struct Widget {
    }

    // Vector for a specified  type
    public struct WidgetVector {
        widgets: vector<Widget>
    }

    // Vector for a generic type 
    public struct GenericVector<T> {
        values: vector<T>
    }

    // Creates a GenericVector that hold a generic type T
    public fun create<T>(): GenericVector<T> {
        GenericVector<T> {
            values: vector::empty<T>()
        }
    }

    // Push a value of type T into a GenericVector
    public fun put<T>(vec: &mut GenericVector<T>, value: T) {
        vector::push_back<T>(&mut vec.values, value);
    }

    // Pops a value of type T from a GenericVector
    public fun remove<T>(vec: &mut GenericVector<T>): T {
        vector::pop_back<T>(&mut vec.values)
    }

    // Returns the size of a given GenericVector
    public fun size<T>(vec: &mut GenericVector<T>): u64 {
        vector::length<T>(&vec.values)
    }
}

3.8.2 table

table是一个映射类的集合,可以动态存储键值对。但与传统的映射集合不同,它的键和值不存储在 Table 值中,而是使用 Sui 的对象系统存储。该 Table 结构仅充当对象系统的句柄以检索这些键和值。(3.9节分析这句话)

table要求键必须有copy+store+drop能力。值必须有store能力

module collection::table {
    use sui::table::{Table, Self};
    use sui::tx_context::{TxContext};

    // Defining a table with specified types for the key and value
    public struct IntegerTable {
        table_values: Table<u8, u8>
    }

    // Defining a table with generic types for the key and value 
    public struct GenericTable<phantom K: copy + drop + store, phantom V: store> {
        table_values: Table<K, V>
    }

    // Create a new, empty GenericTable with key type K, and value type V
    public fun create<K: copy + drop + store, V: store>(ctx: &mut TxContext): GenericTable<K, V> {
        GenericTable<K, V> {
            table_values: table::new<K, V>(ctx)
        }
    }

    // Adds a key-value pair to GenericTable
    public fun add<K: copy + drop + store, V: store>(table: &mut GenericTable<K, V>, k: K, v: V) {
        table::add(&mut table.table_values, k, v);
    }

    /// Removes the key-value pair in the GenericTable `table: &mut Table<K, V>` and returns the value.   
    public fun remove<K: copy + drop + store, V: store>(table: &mut GenericTable<K, V>, k: K): V {
        table::remove(&mut table.table_values, k)
    }

    // Borrows an immutable reference to the value associated with the key in GenericTable
    public fun borrow<K: copy + drop + store, V: store>(table: &GenericTable<K, V>, k: K): &V {
        table::borrow(&table.table_values, k)
    }

    /// Borrows a mutable reference to the value associated with the key in GenericTable
    public fun borrow_mut<K: copy + drop + store, V: store>(table: &mut GenericTable<K, V>, k: K): &mut V {
        table::borrow_mut(&mut table.table_values, k)
    }

    /// Check if a value associated with the key exists in the GenericTable
    public fun contains<K: copy + drop + store, V: store>(table: &GenericTable<K, V>, k: K): bool {
        table::contains<K, V>(&table.table_values, k)
    }

    /// Returns the size of the GenericTable, the number of key-value pairs
    public fun length<K: copy + drop + store, V: store>(table: &GenericTable<K, V>): u64 {
        table::length(&table.table_values)
    }

}

3.8.3 bag

bag本质上和table类似,只不过可以放不同类型的对象:

module collection::bag {

    use sui::bag::{Bag, Self};
    use sui::tx_context::{TxContext};

    // Defining a table with generic types for the key and value 
    public struct GenericBag {
       items: Bag
    }

    // Create a new, empty GenericBag
    public fun create(ctx: &mut TxContext): GenericBag {
        GenericBag{
            items: bag::new(ctx)
        }
    }

    // Adds a key-value pair to GenericBag
    public fun add<K: copy + drop + store, V: store>(bag: &mut GenericBag, k: K, v: V) {
       bag::add(&mut bag.items, k, v);
    }

    /// Removes the key-value pair from the GenericBag with the provided key and returns the value.   
    public fun remove<K: copy + drop + store, V: store>(bag: &mut GenericBag, k: K): V {
        bag::remove(&mut bag.items, k)
    }

    // Borrows an immutable reference to the value associated with the key in GenericBag
    public fun borrow<K: copy + drop + store, V: store>(bag: &GenericBag, k: K): &V {
        bag::borrow(&bag.items, k)
    }

    /// Borrows a mutable reference to the value associated with the key in GenericBag
    public fun borrow_mut<K: copy + drop + store, V: store>(bag: &mut GenericBag, k: K): &mut V {
        bag::borrow_mut(&mut bag.items, k)
    }

    /// Check if a value associated with the key exists in the GenericBag
    public fun contains<K: copy + drop + store>(bag: &GenericBag, k: K): bool {
        bag::contains<K>(&bag.items, k)
    }

    /// Returns the size of the GenericBag, the number of key-value pairs
    public fun length(bag: &GenericBag): u64 {
        bag::length(&bag.items)
    }
}

3.9 动态字段

上文提到:table是一个映射类的集合,可以动态存储键值对。但与传统的映射集合不同,它的键和值不存储在 Table 值中,而是使用 Sui 的对象系统存储。该 Table 结构仅充当对象系统的句柄以检索这些键和值。bag和table是类似的。
如何理解table和bag集合呢,其实table本身的定义如下:

struct Table<phantom K, phantom V> has key, store {
    id: UID,   // 相当于“主键”
    size: u64, // 只是计数
}

添加对象时,做的事情其实是:

field::add(&mut table.id, k, v); // 在 table.id 这棵“父对象”下挂一条动态字段
table.size += 1;

动态字段有两种类型:

1. 动态字段:可以存储任何具有store能力的值,但是存储在这种字段中的对象将被视为被包装过(例如一个带有key能力的全局对象被嵌套进另一个结构体中),无法通过其直接访问通过外部工具(浏览器、钱包等)访问存储的 ID。添加时使用的是 field::add(&mut parent.id, name, child);

2. 动态对象字段:值必须是 Sui 对象(具有 key 和 store 能力,以及 id: UID 作为第一个字段),但仍然可以通过它们的对象 ID 直接访问被附上。添加是使用的是  ofield::add(&mut parent.id, name, child);

4. move进阶!

4.1 dapp

dapp即Decentralization Application,去中心化应用。即DApp = 前端界面 + 智能合约 + 链上数据
其实我们在第二章说的安装都只安装了后端程序,只写了智能合约,发布上链有了链上数据,但作为成熟的应用,没有前端是不行的。
为了能写一个完整的dapp,需要安装一个前端,这里推荐安装node和npm
随后在某个目录下执行npm i @mysten/sui即可开始编写js代码!
编写js代码后可通过node xxx.js执行。