type
Post
status
Published
date
Feb 17, 2025
slug
summary
Sui 训练营 Task 2 的学习笔记,结合 The Move Book 进行了拓展,解释了复制粘贴的代码每一行是什么意思,并给出书的索引方便查阅,最后使用不同方法调用了我们写好的 Coin 合约。
tags
Move
category
Web3
icon
password
Property
Feb 17, 2025 07:11 AM

本文目标

  • 复习上节课的内容,并且解释代码
  • 完成 sui::coin 相关知识的学习
  • 理解独享所有权和共享所有权的区别
  • 创建一个只能指定地址 Mint 的 Coin 合约
  • 创建一个任何人都能 Mint 的 Faucet Coin 合约
  • 尝试使用区块浏览器的函数调用功能
  • 使用 sui client 构建 PTB 命令执行函数调用
 

To-Do List 解析

/// Module: todo_list module todo_list::todo_list { use std::string::String; /// List of todos. Can be managed by the owner and shared with others. public struct TodoList has key, store { id: UID, items: vector<String> } /// Create a new todo list. public fun new(ctx: &mut TxContext): TodoList { let list = TodoList { id: object::new(ctx), items: vector[] }; (list) } /// Add a new todo item to the list. public fun add(list: &mut TodoList, item: String) { list.items.push_back(item); } /// Remove a todo item from the list by index. public fun remove(list: &mut TodoList, index: u64): String { list.items.remove(index) } /// Delete the list and the capability to manage it. public fun delete(list: TodoList) { let TodoList { id, items: _ } = list; id.delete(); } /// Get the number of items in the list. public fun length(list: &TodoList): u64 { list.items.length() } }
写完 Hello World (Hello Move),那么下一步就是对新语言的语法进行学习,这部分内容通常大同小异,最重要的是理解新语言中的新概念。
请先将下面这本书全部快速浏览一遍,不必深入地理解所有东西,但是需要知道什么东西在哪里能找到,然后在实践写代码的过程中,遇到不懂的概念就回去查,深入理解。带着需求学习,这是我的方法。
下面按照以上思路,逐段理解我们之前复制粘贴的 To-Do List 代码。

包(Package)和模块(Module)

Move 是一种用于编写智能合约的语言,这些合约被存储和运行在区块链上。一个程序通常被组织成一个包 (package)。包被发布到区块链上,并通过一个 地址 进行标识。已发布的包可以通过发送交易来调用其函数进行交互。它还可以作为其他包的依赖项。
/// Module: todo_list module todo_list::todo_list { use std::string::String; ...... }
使用 module 关键字后跟包地址、模块名称和模块体在花括号 {} 内来声明模块。模块名称应采用 snake_case 形式,即所有小写字母,单词之间用下划线分隔。模块名称在包内必须是唯一的。
通常,sources/ 文件夹中的单个文件包含一个模块。文件名应与模块名称匹配 - 例如,donut_shop 模块应存储在 donut_shop.move 文件中。
在 Move 语言中发布的包都是以包(Package)和模块(Module)结构组成的。
package 0x... module a struct A1 fun hello_world() module b struct B1 fun hello_package()
包由模块组成 - 单独的作用域,包含函数、类型和其他项。模块是 Move 中的代码组织基本单元。模块用于组织和隔离代码,模块的所有成员默认情况下对模块私有。
模块的成员包括结构体、函数、常量和导入:
参考:
 
use std::string::String;
这里导入了 String ,其他关于导入的语法,
参考:
小结:这段代码声明了模块 todo_list , 从 std::string 导入了 String
 

结构体(Struct)、对象(Object)、能力(Ability)和可见修饰符

/// List of todos. Can be managed by the owner and shared with others. public struct TodoList has key, store { id: UID, items: vector<String> }
这部分内容声明了一个结构体。同时这个结构体还拥有能力 key ,所以它还是一个对象。
每个对象,或者说,有能力 key 的结构体,都需要一个 UID ,也就是 Unique ID,因为每一个对象都是独一无二的。
声明结构体需要 struct 关键字,它之前的 public 关键字是可见修饰符,表示这个结构体可以被外部访问。
这个结构体还有 store 能力,store是一种特殊的能力,允许将类型存储在对象中。该能力是字段可以在具有key能力的结构体中使用的必需条件。换句话说,store能力允许值被包装在对象中。
 
小结:这段代码声明了一个结构体,这个结构体是一个对象,并且可以被外部访问,也可以被包装在对象中存储,结构体内部有一个 id 作为 UID ,也就是该对象的唯一标识符,还有一个 items ,这是一个由字符串构成的向量。
 
这部分内容可以参考,结构体:
能力:
对象:
能力 key :
可见修饰符:
能力 store:
 

函数(Function)、事务上下文(TxContext)和 UID 的生成

/// Create a new todo list. public fun new(ctx: &mut TxContext): TodoList { let list = TodoList { id: object::new(ctx), items: vector[] }; (list) }
函数是 Move 程序的基本构建块。它们可以从用户交互中调用,也可以从其他函数中调用,并将可执行的代码组织成可重用的单元。函数可以接受参数并返回值。在模块级别,它们使用 fun 关键字声明。默认情况下,它们是私有的,只能在模块内部访问。
如果函数的第一个参数是模块内部的结构体,则可以使用 . 运算符调用该函数。如果函数使用另一个模块中的结构体,则默认不会将方法与结构体关联起来。在这种情况下,可以使用标准的函数调用语法来调用该函数。
方法是特殊的函数。
 
小结:这段代码声明了一个名称为 new 函数,这个函数接受一个 &mut TxContext 类型的参数,输出一个 TodoList 类型的结构体。
TxContext 就是事务上下文,也就是这个交易的上下文。在这里,它作为一个参数传到函数里。
函数体中,使用 let 关键词,将一个 TodoList 结构体赋值给变量 list ,最后将 list 作为返回值。
其中 id: object::new(ctx) 交代了 UID 的生成方式。
  • UID是从tx_hash和递增的index派生而来的。
  • derive_id函数在sui::tx_context模块中实现,因此生成UID需要TxContext。
  • Sui验证器不允许使用未在同一函数中创建的UID。这防止了在对象被解包后预先生成和重复使用UID。
 
函数:
结构体方法:
UID 的生成:
事务上下文:
 

所有权(Ownership)、引用(References)和可变引用

/// Add a new todo item to the list. public fun add(list: &mut TodoList, item: String) { list.items.push_back(item); }
这里涉及了一个 Move 语言跟其他语言很不一样的概念,即所有权概念。(当然,Rust语言是祖师爷。)
下面引用书中的例子,地铁票应用,
我们将介绍 4 种不同的场景:
  1. 乘客可以在售票亭以固定价格购买地铁票。
  1. 可以向检票员出示地铁票以证明乘客拥有有效票卡。
  1. 可以在地铁闸口使用地铁票进入地铁,并扣除一次乘车次数。
  1. 地铁票用完后可以回收。
地铁票应用的初始结构很简单。我们定义了 Card 类型和代表单张地铁票乘坐次数的常量 USES。我们还添加了一个错误常量,用于处理地铁票用完的情况。
module book::metro_pass { /// Error code for when the card is empty. const ENoUses: u64 = 0; /// Number of uses for a metro pass card. const USES: u8 = 3; /// A metro pass card public struct Card { uses: u8 } /// Purchase a metro pass card. public fun purchase(/* pass a Coin */): Card { Card { uses: USES } } }

引用

引用是一种向函数展示值而不放弃拥有权的方式。 在我们的例子中,当我们将地铁票出示给检票员时,我们不想放弃对它的拥有权,也不允许他们扣除乘车次数。我们只想允许检票员读取地铁票信息并验证其有效性。
为了做到这一点,在函数签名中,我们使用符号 & 表示我们传递的是值的引用,而不是值本身。
/// Show the metro pass card to the inspector. public fun is_valid(card: &Card): bool { card.uses > 0 }
现在,函数无法获得地铁票的所有权,也不能扣除乘车次数。但是它可以读取地铁票信息。值得注意的是,这样的函数签名使得不带地铁票调用该函数变得不可能。这是一个重要的特性,它允许我们在下一章节讨论的 能力模式。

可变引用

在某些情况下,我们希望允许函数更改地铁票的值。例如,当我们在闸口使用地铁票时,我们想要扣除一次乘车次数。为了实现这一点,我们在函数签名中使用关键字 &mut
/// Use the metro pass card at the turnstile to enter the metro. public fun enter_metro(card: &mut Card) { assert!(card.uses > 0, ENoUses); card.uses = card.uses - 1; }
正如您在函数体中看到的,&mut 引用允许修改值,函数可以扣除乘车次数。

按值传递

最后,让我们来看一下将值本身传递给函数会发生什么。在这种情况下,函数获取该值的拥有权,并且原始作用域将无法再使用它。地铁票的所有者可以回收它,因此失去拥有权。
/// Recycle the metro pass card. public fun recycle(card: Card) { assert!(card.uses == 0, ENoUses); let Card { uses: _ } = card; }
在 recycle 函数中,地铁票被按值获取,可以解包并销毁。原始作用域无法再使用它。

完整示例

为了展示应用程序的完整流程,让我们将所有部分组合成一个测试。
#[test] fun test_card_2024() { // declaring variable as mutable because we modify it let mut card = purchase(); card.enter_metro();// modify the card but don't move it assert!(card.is_valid(), 0);// read the card! card.enter_metro();// modify the card but don't move it card.enter_metro();// modify the card but don't move it card.recycle();// move the card out of the scope }
 

解构结构体

/// Remove a todo item from the list by index. public fun remove(list: &mut TodoList, index: u64): String { list.items.remove(index) } /// Delete the list and the capability to manage it. public fun delete(list: TodoList) { let TodoList { id, items: _ } = list; id.delete(); } /// Get the number of items in the list. public fun length(list: &TodoList): u64 { list.items.length() }
这几段代码比较简单,需要注意的是,结构体默认是非可丢弃的,这意味着初始化的结构体值必须被使用:要么存储,要么进行解构。解构结构体意味着将其拆解为其各个字段。可以使用 let 关键字后跟结构体名称和字段名称来完成解构。如上述 delete 函数。
 
解构结构体:
 
到这里,我们就能完全理解上节课我们复制粘贴的代码到底在做什么了。
下面开始 Coin 库的学习。
 

介绍代币、Coin 协议和货币

代币在加密货币世界中起着关键作用,作为价值或资产的数字表示。它们类似于数字证书,授予对各种资产的所有权或访问权,无论是有形资产还是虚拟资产, 所有这些都安全地记录在区块链上。以太坊区块链上的一个著名标准是 ERC-20。

代币 Token

这些数字资产仅以电子形式存在于区块链上,即去中心化的数字账本。 它们代表有价值的东西,无论是加密货币、公司股份、去中心化组织的投票权,甚至是游戏中的虚拟收藏品。 代币可以在区块链上转移或交易,其所有权和交易透明地记录在案。由于智能合约的存在,一些代币可以自动化诸如红利分配或特定生态系统内的服务访问等流程 。首次代币发行(ICOs)通常使用代币作为筹资手段,投资者购买代币期望其未来价值的升值。

Coin 协议

ERC-20 标准是关于如何实现代币的第一个规范之一,定义了一组代币在以太坊区块链上必须遵守的接口函数。 这些函数提供了与代币交互的通用框架,并确保不同应用程序和钱包之间的互操作性。以下是最重要的 ERC-20 接口函数的摘要:
  1. totalSupply(): 该函数返回流通中的代币总供应量。
  1. balanceOf(address _owner): 它允许你检查特定以太坊地址拥有的代币余额。
  1. transfer(address _to, uint256 _value): 该函数使得从发送者地址向另一个地址转移指定数量的代币成为可能。
尽管 ERC-20 标准易于实现,但所有开发人员都需要提供自己的实现方式,并且最终会一次又一次地编写几乎相同的代码。 这是由于以太坊网络(以及许多其他 EVM 网络)的限制,那里没有核心智能合约可以作为开发人员使用的库,类似于许多语言(如 Java)中的核心库。
Move 通过在 0x2 直接定义 Coin 标准解决了这个问题,使开发人员可以直接定义和管理代币,而不必每次都重新编写实现。Sui 网络上的代币称为 Coin
另一个 Coin 设计中的关键设计原则是反映现实世界中货币的自然设计。 例如,当某人收到 $1 时,他们可以将这 $1 放进口袋并稍后取出。这与 EVM 链中的代币余额情况不同,在 EVM 链中,所有余额都记录在定义 USDC 代币(与美元挂钩的代币)的智能合约中。 这种集中式余额设计更类似于银行存款系统,所有余额仅在银行系统中定义。这给新接触加密货币的用户带来了很多困惑, 因为他们认为自己的钱包(例如硬件钱包)实际上持有各种代币。
在 Move 中,Coin 更自然且易于理解——当用户收到 Coins 时,这些 Coins 实际上存储在属于该用户的对象中(可以被认为是钱包)。 用户可以稍后轻松地从该对象中取出 Coins 并随意使用它们。
 

创建 Coin

 
在 Move 中,开发人员只需调用一个模块(智能合约)来创建和管理他们的代币。为了区分不同开发人员创建的不同类型的代币,Coin 使用了泛型(类型参数):
/// 获取代币余额的不可变引用。 public fun balance<T>(coin: &Coin<T>): &Balance<T> { &coin.balance }
上述函数用于检查用户所拥有的代币钱包对象的余额。注意,函数名称末尾有一个 <T>
这个 balance 函数接受一个 Coin<T> 的引用,这个 <T> 是一个泛型参数。
 
泛型:
 
为了创建代币,开发人员首先需要在他们的模块中将这种 Coin 类型定义为一个结构体:
module my_coin::my_coin { public struct MYCOIN has drop {} }
drop 能力通常在自定义的集合类型上使用,以消除在不再需要集合时的特殊处理需求。例如,vector 类型具有 drop 能力,这使得在不再需要时可以忽略该向量。然而,Move类型系统最大的特点是能够没有 drop。这确保了资产得到正确处理,而不被忽略。
一个仅具有 drop 能力的结构体称为 Witness
能力 drop:
 
这类似于 SUI 代币的定义方式。 然后,开发人员可以通过调用 coin::create_currency 创建新的代币,通常作为 init 函数的一部分, 因为你需要一个 Coin 类型的一次性见证(otw)对象(在这种情况下为 MYCOIN):
use std::string; use sui::url; fun init(otw: MYCOIN, ctx: &mut TxContext) { let (treasury_cap, metadata) = coin::create_currency( otw, 9, b"MYC", b"MyCoin", b"My Coin description", option::some(url::new_unsafe(string::utf8(b"https://mycoin.com/logo.png"))), ctx, ); transfer::public_freeze_object(metadata); transfer::public_transfer(treasury_cap, tx_context::sender(ctx)); }
在许多应用程序中,一个常见的用例是仅在发布包时运行某些代码。想象一个简单的商店模块,需要在发布时创建主商店对象。在 Sui 中,这可以通过在模块中定义一个init函数来实现。当模块发布时,该函数将自动被调用。
如果模块中存在init函数并遵循以下规则,则在发布时会调用该函数:
  • 函数必须命名为init,是私有的并且没有返回值。
模块初始化器 init:
 
coin::create_currency 返回一个元数据对象,用于存储关于代币的信息:符号(Coin 将显示的缩写)、 名称、描述和 logo URL。这允许链外组件(如 Web UI)查找并显示这些信息。开发人员可以选择冻结元数据对象, 这样名称/符号等就不能再更改,或者保留它的所有权并转移到一个账户以供以后管理(更多内容将在后续课程中介绍)。
coin::create_currency 还返回一个 TreasuryCap 对象,可用于管理代币。我们将在后续课程中详细讨论这个内容。
现在代币已经创建,开发人员可以在调用代币函数时使用 MYCOIN 作为 Coin 类型参数,例如:
public fun my_coin_balance(coin: &Coin<MYCOIN>): &Balance<MYCOIN> { // <MYCOIN> is technically not required here as the type can be inferred. // It's just included explicitly for demonstration purposes. coin::balance<MYCOIN>(coin) }
 

可选类型 Option

option::some(url::new_unsafe(string::utf8(b"https://mycoin.com/logo.png"))),
Option 是一种表示可选值的类型,它可能存在,也可能不存在。Move 中的 Option 概念借鉴自 Rust,它是 Move 中非常有用的原语。Option 在标准库中定义,如下所示:
文件:move-stdlib/source/option.move
// 文件:move-stdlib/source/option.move/// 可能存在,也可能不存在的值的抽象。 struct Option<Element> has copy, drop, store { vec: vector<Element> }
可选类型:
 

受限与公开转移

transfer::public_freeze_object(metadata); transfer::public_transfer(treasury_cap, tx_context::sender(ctx));
这里我们将 metadata 置于不可变状态成为公共变量,永远无法改变。并将 TreasuryCap 对象转移给发送者。
 
存储功能:
 
受限与公开转移:
 

TreasuryCap

我们创建了我们的第一个代币,并将 TreasuryCap 对象暂时转移给发送者(模块的部署者)。通过这个 TreasuryCap,该账户现在可以铸造 MYCOIN 代币:
use std::string; use sui::url; fun init(otw: MYCOIN, ctx: &mut TxContext) { let (treasury_cap, metadata) = coin::create_currency( otw, 9, b"MYC", b"MyCoin", b"My Coin description", option::some(url::new_unsafe(string::utf8(b"https://mycoin.com/logo.png"))), ctx, ); transfer::public_freeze_object(metadata); transfer::public_transfer(treasury_cap, tx_context::sender(ctx)); } entry fun mint(treasury_cap: &mut TreasuryCap<MYCOIN>, ctx: &mut TxContext) { let coins = coin::mint(treasury_cap, 1000, ctx); // Do something with the coins }
有两种入口函数—— public entry 和 entry
我们需要学习唯一的新函数类型是私有入口函数(简称入口函数),它只能直接从交易中调用,不能从其他 Move 代码中调用。
私有入口函数对于开发者希望直接向用户提供的功能非常有用,这些功能只能作为交易的一部分调用, 而不能在其他模块中调用。一个例子是剪票——我们希望用户必须显式地将其作为交易的一部分来调用, 不希望其他模块代表用户剪票。后者对于用户来说更难检测,他们可能不会期望在发送交易时会发生这种情况。
入口函数:
 
有四个重要的点需要指出:
  1. coin::mint 创建一个新的 Coin(钱包)对象。这意味着用户其他钱包中的现有余额不会改变。
  1. 如果你记得,归属对象在作为参数传递给交易时会进行验证,并且只有它们的所有者才能这样做。在这种情况下,只有拥有 TreasuryCap 的账户可以调用 mint
  1. TreasuryCap 也有一个类型参数(MYCOIN)。这指定了国库上限管理的代币类型。
  1. coin::mint 不需要指定 MyCoin 作为类型参数,因为编译器可以从 treasury_cap(类型为 TreasuryCap)中推断出来。
还需要注意的是,TreasuryCap 的类型是一个完全限定的类型名——例如,如果我们的模块地址是 0x123,那么它的类型是 0x123::my_coin::MYCOIN。这意味着如果其他人在他们的模块中创建了一个名为 MYCOIN 的结构体,即使结构体名称相同,也会被视为完全不同的代币。除了 coin::mint,开发人员还可以使用 coin::mint_and_transfer 直接铸造并转移到指定账户。 另一种常见的模式是在 init 函数中铸造初始分配的代币:
use std::string; use sui::url; fun init(otw: MYCOIN, ctx: &mut TxContext) { let (treasury_cap, metadata) = coin::create_currency( otw, 9, b"MYC", b"MyCoin", b"My Coin description", option::some(url::new_unsafe(string::utf8(b"https://mycoin.com/logo.png"))), ctx, ); coin::mint_and_transfer(treasury_cap, 1000000, tx_context::sender(ctx), ctx); transfer::public_freeze_object(metadata); transfer::public_transfer(treasury_cap, tx_context::sender(ctx)); }
这允许开发人员创建初始数量的代币以供流通。他们可以选择实现一个铸币函数,以便以后创建更多的代币。
这样我们实际上已经实现了 Task 2 中的,只能指定地址 Mint 的 Coin
下面是我的实现:
module my_coin::my_coin { use std::option; use sui::coin; use sui::transfer; use sui::tx_context::{Self, TxContext}; use sui::url::{Self, Url}; // Name matches the module name but in UPPERCASE public struct MY_COIN has drop {} // Module initializer is called once on module publish. // A treasury cap is sent to the publisher, who then controls minting and burning. fun init(witness: MY_COIN, ctx: &mut TxContext) { let (treasury, metadata) = coin::create_currency( witness, 9, b"MYC", b"My Coin description", b"", option::some(url::new_unsafe_from_bytes(b"https://silver-blushing-woodpecker-143.mypinata.cloud/ipfs/Qmed2qynTAszs9SiZZpf58QeXcNcYgPnu6XzkD4oeLacU4")), ctx ); transfer::public_freeze_object(metadata); transfer::public_transfer( treasury, tx_context::sender(ctx) ) } public entry fun mint( treasury: &mut coin::TreasuryCap<MY_COIN>, amount: u64, recipient: address, ctx: &mut TxContext ) { coin::mint_and_transfer(treasury, amount, recipient, ctx) } }
 
发布之后在区块浏览器上找到
notion image
发现 SuiVision 这个区块浏览器给出图形化调用函数的界面,试用一下~
 
先在本地运行,导出你地址的私钥,并将私钥导入到你的浏览器钱包里。
sui keytool export --key-identity <your-address>
 
接下来开始填参数
第一个参数是 TreasuyCap 对象的ID,从创建合约交易的 Changes 可以找到,就是下图的 0xaa… ;第二个参数是数量,随意写,我写了1000个;第三个参数是接收者的地址,我写了自己的地址。
notion image
 
执行,弹出“是否批准这个交易?”,因为我填了1000,但因为单位的关系,实际上1e9才是1个,所以这里显示我铸造了,1e-6个,是正常现象。
notion image
批准之后,交易就执行啦~
 

扩展任务:Burn 函数

增加 Burn 的功能。
需要传入之前生成的 TreasuryCap id
entry fun burn<T>(cap: &mut TreasuryCap<T>, c: Coin<T>): u64 { let Coin { id, balance } = c; id.delete(); cap.total_supply.decrease_supply(balance) }
 
 
 

发行水龙头 Coin

我们已经学习了如何使用生成的 TreasuryCap 对象来铸造代币。 然而,只有 TreasuryCap 的所有者可以调用它。如果我们想允许用户自由铸造代币(可能有一定的限制)呢?这该如何实现?
如果你查看 TreasuryCap 对象结构体的定义,它具有 store 能力:
/// 允许持有者铸造和销毁类型为 `T` 的代币的能力。可转让。 public struct TreasuryCap<phantom T> has key, store { id: UID, total_supply: Supply<T> }
 
其中 <phantom T> 是虚拟类型参数
虚拟类型参数:
 
这意味着,它可以存储在其他结构体和对象中!因此,解决方案是将其包装在一个任何人都可以访问并提供给铸币函数作为参数的共享对象中:
use std::string; use sui::url; public struct MYCOIN has drop {} public struct TreasuryCapHolder has key { id: UID, treasury_cap: TreasuryCap<MYCOIN>, } fun init(otw: MYCOIN, ctx: &mut TxContext) { let (treasury_cap, metadata) = coin::create_currency( otw, 9, b"MYC", b"MyCoin", b"My Coin description", option::some(url::new_unsafe(string::utf8(b"https://mycoin.com/logo.png"))), ctx, ); transfer::public_freeze_object(metadata); let treasury_cap_holder = TreasuryCapHolder { id: object::new(ctx), treasury_cap, }; transfer::share_object(treasury_cap_holder); } entry fun mint(treasury_cap_holder: &mut TreasuryCapHolder, ctx: &mut TxContext) { let treasury_cap = &mut treasury_cap_holder.treasury_cap; let coins = coin::mint(treasury_cap, 1000, ctx); // 用coin做点什么 }
在上述示例中,我们将 TreasuryCap 包装在一个共享的 TreasuryCapHolder 对象中。现在任何人都可以将 &mut TreasuryCapHolder 传递给铸币函数,自由地铸造 MYCOIN 代币。实际上,开发人员应该添加一些限制,例如总共可以铸造多少代币、每个用户可以铸造多少代币、基于白名单的控制等。
如在对象课程中讨论的那样,除了将 TreasuryCap 对象包装到另一个持有对象中之外,我们还可以利用动态字段、动态对象字段甚至对象所有权来保留 TreasuryCap。然而,包装通常是首选,因为在访问时它更简单。
下面是直接使用对象所有权的实现:
module faucet_coin::mycoin { use std::option; use sui::coin::{Self, TreasuryCap}; use sui::transfer; use sui::tx_context::{Self, TxContext}; /// The type identifier of coin. The coin will have a type /// tag of kind: `Coin<package_object::mycoin::MYCOIN>` /// Make sure that the name of the type matches the module's name. public struct MYCOIN has drop {} /// Module initializer is called once on module publish. A treasury /// cap is sent to the publisher, who then controls minting and burning fun init(witness: MYCOIN, ctx: &mut TxContext) { let (treasury, metadata) = coin::create_currency(witness, 6, b"MYCOIN", b"", b"", option::none(), ctx); transfer::public_freeze_object(metadata); transfer::public_share_object(treasury) } entry public fun mint( treasury_cap: &mut TreasuryCap<MYCOIN>, amount: u64, recipient: address, ctx: &mut TxContext, ) { let coin = coin::mint(treasury_cap, amount, ctx); transfer::public_transfer(coin, recipient) } }
区别在于这里将 treasury 变为共享对象,Sui 引入了四种不同的对象所有权类型
  • 单一所有者: 对象由单个账户拥有,授予对对象的独占控制权。
  • 共享状态: 对象可以与网络共享,允许多账户读取和修改对象。
  • 不可变状态: 对象变为永久只读,提供稳定且恒定的状态。
  • 对象所有者: 对象可以拥有其他对象,实现复杂的关系和模块化系统。
所有权:
 
这次我们使用 Sui Client 构造 PTB 进行交互
先在发布包之后的输出中找到 PACKAGE_ID
export PACKAGE_ID=0x47394d0edc6e18ed385f67283bb07d258600a4c595af5abd1bbcafa2011799e2
在将此 ID 输入区块浏览器,找到 TreasuryCap 对象的 ID
export TREASURY_CAP_ID=0x4a0c09bbfc34931585d12a8cd15fc20d96e3cc3696bdcd218cfea035aa9e03e3
为了测试非发布者是否能进行 Mint,我们换一个地址:
查看当前客户端有几个地址
sui keytool list
如果只有一个, 新生成一个,别忘了领水
sui keytool generate ed25519
然后切换地址
sui client switch --address <your-new-address>
查看余额
sui client balance
确认有 Gas 之后,构造交易:
export MY_ADDRESS=0x7fc0799d79e88e2abfaa579a2d25cc09d12ec16c5bcf0c3a6be741225e7e681f
 
sui client ptb \ --gas-budget 100000000 \ --assign sender @$MY_ADDRESS \ --assign treasuryCap @$TREASURY_CAP_ID \ --move-call $PACKAGE_ID::mycoin::mint treasuryCap 1000000000 sender
执行成功,在区块浏览器中找到刚刚 Mint 的 MYCOIN
notion image

参考文章

 
致谢:
💡
有关Notion安装或者使用上的问题,欢迎您在底部评论区留言,一起交流~
 
 
Move 语言学习记录(三)Move 语言学习记录(一)