本文作者:MoveMoon[1]
Sui Move 初体验系列文章包含:
让我在 2019 年对 Web3 场景感兴趣的是阅读 Facebook(现在的 Meta)备受期待的 Libra 白皮书。Libra 是 Facebook 在区块链技术方面的新尝试,其深远的目标是为数十亿用户实现一个简单的全球货币和金融基础设施。 然而,Libra 协会的参与者却受到威胁,要面对各种监管机构的高度审查。尽管 Facebook 重组了更名后的Libra 协会[2],并清盘了该项目[3],但这家位于门罗帕克的科技巨头探索 Metaverse 和 Web3 世界的使命并没有白费。前工程师们成立了第 1 层区块链,继续发展他们的想法!
有大量的 Move 支撑的 L1 区块链[4]试图向加密货币场景介绍自己,但值得注意的是业界现在正在关注的是Aptos[5]和Mysten Lab 的 Sui[6]。他们都在协议下运行 Move VM。我最近对 Sui Network 感兴趣,因为它声称是有吸引力的 L1 智能合约平台,通过横向扩展实现高吞吐量。
自称是 Libra 区块链后继者的共同点是使用Move 编程语言[7],最初由 Facebook 的 Libra 团队开发。Solidity[8]现在是区块链语言领域的主导者。Solidity 是作为第一批利用众所周知的数据类型和数据结构来实现核心编程语言原则的区块链语言之一。
而且,虽然该语言自然都是关于资产转移的,但它没有对资产的本地支持。如果区块链语言的主要目标是数字资产活动,那么主要特征是安全[9]。
如果你熟悉用现代语言写代码,一等公民的概念应该引起你的注意。JavaScript 中的函数是一等公民的对象。所有的函数都是 JavaScript 中的对象,它们继承自 "Object "原型,并被赋予键值对。这些对象可以被分配给变量,并作为一个参数传递。
Sam Blackshear 的演讲[10],他以前是 Novi Research,现在是 Mysten Lab 的 CTO
Move 编程语言旨在解决这些问题而设计的。[11]Move 的原则是维护数字资产成为一等公民,它是为管理区块链上代表的数字资产而明确设计的。
从技术上讲,合约中用于存储、分配、函数和进程的参数或返回值的变量都可以是数字资产[12] (来自 Sui 文档)。由于 Move 的静态类型,编译器可以在编译期间和部署前评估大多数资源问题,这增加了智能合约的安全性。
当仔细研究 EVM 和 Move 之间的数据模型差异时,EVM 的资产被编码在一个动态索引的映射里面,如所有者_地址 -> <bytes资产>
。任何任意的资产都表示为 HashMap 中的条目,这意味着更新状态只是改变集合中的条目值。
数字资产是无法摆脱定义它们的合约的。但是,它们可以说是 Web3 领域中的一等公民,应该有权在语言层面上用一个专门的类型来表示。
让 Move 语言变得有趣的是,Move 资产是任意的用户定义的类型。这些资产被 Move 对象所利用,这些对象可以通过一系列的交易来改变其状态。Sui 文档还表示,该语言提供了内置的资源安全,允许数字资产在保持其完整性的情况下轻松地跨越合约边界流动。
该语言还具有数据可组合性的优点。制作一个包含另一个资产的新资产总是可行的。定义一个通用的包装器Z(T)
也是可行的,它能够包装任何资产,提供或结合新的属性到一个被包装的对象。
更不用说,Move 提供了一个吸引人的测试工具[13],它被整合到语言层面。如果你来自传统的 Web 开发背景(也被称为 "Web2"),那么这个单元测试框架被设计为可以随时随地利用。
与你所希望的相反,当下智能合约语言缺乏定义的单元测试或 E2E 测试的结构。同时,Move 的内置测试框架在编译层面保证了类型和资产安全,就像 Rust 的测试框架或 Java 的 JUnit 框架一样。
让我们看看下面的三明治例子[14]来挖掘数据的可组合性。该代码定义了结构体(资产)火腿(Ham)、面包(Bread)、三明治(Sandwich)和杂货店(Grocery)。GreceryOwnerCapability结构体允许所有者提取任何利润。
杂货店的结构在下面的代码片断上,该结构内有利润和余额。在声明了火腿和面包的价格后,模块init进行了杂货店的创建,然后将GreceryOwnerCapability转移到交易发送者。
struct Ham has key {
id: VersionedID
}
struct Bread has key {
id: VersionedID
}
struct Sandwich has key {
id: VersionedID,
}
// This Capability allows the owner to withdraw profits
struct GroceryOwnerCapability has key {
id: VersionedID
}
// Grocery is created on module init
struct Grocery has key {
id: VersionedID,
profits: Balance<SUI>
}
/// Price for ham
const HAM_PRICE: u64 = 10;
/// Price for bread
const BREAD_PRICE: u64 = 2;
/// Not enough funds to pay for the good in question
const EInsufficientFunds: u64 = 0;
/// Nothing to withdraw
const ENoProfits: u64 = 1;
/// On module init, create a grocery
fun init(ctx: &mut TxContext) {
transfer::share_object(Grocery {
id: tx_context::new_id(ctx),
profits: balance::zero<SUI>()
});
transfer::transfer(GroceryOwnerCapability {
id: tx_context::new_id(ctx)
}, tx_context::sender(ctx));
}
下面的代码用一个假设的 CoinC
来购买一些火腿 🍖 和一些面包 🍞,然后将火腿和面包组合成一个三明治 🥪。尽管删除了现有的两种食物,但却创造了一个美味的三明治!
/// Exchange `c` for some ham
public entry fun buy_ham(
grocery: &mut Grocery,
c: Coin<SUI>,
ctx: &mut TxContext
) {
let b = coin::into_balance(c);
assert!(balance::value(&b) == HAM_PRICE, EInsufficientFunds);
balance::join( &mut grocery.profits, b);
transfer::transfer(Ham {
id: tx_context::new_id(ctx)
},
tx_context::sender(ctx))
}
/// Exchange `c` for some bread
public entry fun buy_bread(
grocery: &mut Grocery,
c: Coin<SUI>,
ctx: &mut TxContext
) {
let b = coin::into_balance(c);
assert!(balance::value(&b) == BREAD_PRICE, EInsufficientFunds);
balance::join( &mut grocery.profits, b);
transfer::transfer(Bread {
id: tx_context::new_id(ctx)
},
tx_context::sender(ctx))
}
/// Combine the `ham` and `bread` into a delicious sandwich
public entry fun make_sandwich(
ham: Ham, bread: Bread, ctx: &mut TxContext
) {
let Ham {
id: ham_id
} = ham;
let Bread {
id: bread_id
} = bread;
id::delete(ham_id);
id::delete(bread_id);
transfer::transfer(Sandwich {
id: tx_context::new_id(ctx)
},
tx_context::sender(ctx))
}
从上面可以看出,当顾客购买火腿或面包时,杂货店结构会收到钱。经营者可以通过提供他的杂货店能力而获益。这似乎类似于 Solana 上常见的 PDA 想法。
/// See the profits of a grocery
public fun profits(grocery: & Grocery): u64 {
balance::value( & grocery.profits)
}
/// Owner of the grocery can collect profits by passing his capability
public entry fun collect_profits(_cap: & GroceryOwnerCapability, grocery:
&
mut Grocery, ctx: &mut TxContext) {
let amount = balance::value( & grocery.profits);
assert!(amount > 0, ENoProfits);
// Take a transferable `Coin` from a `Balance`
let coin = coin::take( &mut grocery.profits, amount, ctx);
transfer::transfer(coin, tx_context::sender(ctx));
}
#[test_only]
public fun init_for_testing(ctx: &mut TxContext) {
init(ctx);
}
我们来做测试。提醒一下,火腿的价格是 10,而面包的价格是 2,净利润是 12。在测试中,代码购买了一片火腿和面包。应该检查杂货店是否有 12 个利润;从店主那里收取利润后,余额为零。
#[test_only]
module basics::test_sandwich {
use basics::sandwich::{
Self,
Grocery,
GroceryOwnerCapability,
Bread,
Ham
};
use sui::test_scenario;
use sui::coin::{
Self
};
use sui::sui::SUI;
#[test]
fun test_make_sandwich() {
let owner = @0x1;
let the_guy = @0x2;
let scenario = &mut test_scenario::begin( & owner);
test_scenario::next_tx(scenario, & owner); {
sandwich::init_for_testing(test_scenario::ctx(scenario));
};
test_scenario::next_tx(scenario, & the_guy); {
let grocery_wrapper = test_scenario::take_shared < Grocery > (scenario);
let grocery = test_scenario::borrow_mut( &mut grocery_wrapper);
let ctx = test_scenario::ctx(scenario);
sandwich::buy_ham(
grocery,
coin::mint_for_testing<SUI>(10, ctx),
ctx
);
sandwich::buy_bread(
grocery,
coin::mint_for_testing<SUI>(2, ctx),
ctx
);
test_scenario::return_shared(scenario, grocery_wrapper);
};
test_scenario::next_tx(scenario, & the_guy); {
let ham = test_scenario::take_owned < Ham > (scenario);
let bread = test_scenario::take_owned < Bread > (scenario);
sandwich::make_sandwich(ham, bread, test_scenario::ctx(scenario));
};
test_scenario::next_tx(scenario, & owner); {
let grocery_wrapper = test_scenario::take_shared < Grocery > (scenario);
let grocery = test_scenario::borrow_mut( &mut grocery_wrapper);
let capability =
test_scenario::take_owned < GroceryOwnerCapability > (scenario);
assert!(sandwich::profits(grocery) == 12, 0);
sandwich::collect_profits( & capability, grocery,
test_scenario::ctx(scenario));
assert!(sandwich::profits(grocery) == 0, 0);
test_scenario::return_owned(scenario, capability);
test_scenario::return_shared(scenario, grocery_wrapper);
};
}
}
与 Mysten 实验室联合创始人兼 CTO、Move 语言的创造者 Sam Blackshear 的炉边谈话[15]
核心区别在于,Sui 使用自己的以对象为中心的全局存储,作为一个对象池运行。当你发布一个模块时,它被保存在一个新创建的模块地址中。当一个新的资源产生时,它被保存到某个账户的地址中。链上存储既费钱又受限制,所以在 Sui Move 中没有全局存储。Sui Move 不允许任何与全局存储有关的行动。当发布一个模块时,它被保存在 Sui 存储中。新生产的项目被保存在 Sui 存储中。
此外,Sui 不需要用地址类型来表示 Sui 中的账户,因为地址在 Move 中不提供全局存储。相反,地址类型被用来表示任何人都可以创建、被复制和被删除的对象 ID。例如,当从模块中创建一个新的“剑”对象时,每个对象都有一个不同的地址,它起到一个标识符的作用。
由于每个对象都有 Key,全局唯一的 ID 为内部 Move 对象上跨越 Move-Sui 边界的铺平了道路,Key
能力也起着举足轻重的作用,作为全局存储操作的 key。它关乎所有全局存储操作,类型必须具有key
能力。每个对象必须有一个唯一的 ID。
Sui 重新设计了 Move,以排除全局存储的使用;ID 类型包括对象 ID 以及序列号,以确保 ID 字段是不可改变的,不能被转移到其他对象。每个交易本质上都是传入一个对象,然后合约修改、销毁或创建新的对象。
由于 Move 模块被发布到 Sui 存储中,Sui 运行时会执行一次自定义初始化函数,该函数在模块发布时被选择性地定义在模块中,目的是预先初始化模块的特定数据,如创建单例对象:它的作用类似于其他面向对象语言(如 Java)中的 "构造函数",从类的元数据中创建一个实例。
最后,入口点将对象引用作为输入,这与 CosmWasm 或 Spring MVC 模式中的 Controller 的概念有些类似。Sui 提供了可从 SUI 模块直接调用的入口函数,以及可从其他函数调用的函数。其中一个最简单的入口函数被定义为处理代表特定用户的地址之间的 gas 对象转移。
不多说了,让你的双手沾上泥土将有助于更清楚地领会 Sui 的 Move 概念。本文将建立两个例子;铸造一个简单的 NFT 和锻造一把剑。
首先,通过运行以下命令安装 Sui binaries[16]。在安装 Sui 之前,你应该先安装Rust and Cargo[17] 工具链。
curl https://raw.githubusercontent.com/MystenLabs/sui/main/doc/utils/sui-setup.sh -o sui-setup.sh
chmod 755 sui-setup.sh
./sui-setup.sh
echo $PATH
cargo install --git https://github.com/move-language/move move-analyzer
在撰写本文时(2022 年 7 月 9 日),devnet[18]是 Sui 唯一可用的网络选项。通过输入以下命令找到你的地址。
wallet active-address
正如在其他协议上所做的那样,你需要在 Discord 的#devnet-faucet[19]上申请测试 SUI 代币。如果你还没有加入频道here[20],请求测试 SUI 代币,然后在Sui Explorer[21]上检查你的交易哈希。
吸引我的一点是,使用 Sui CLI 铸造一个简单的 NFT 例子是原生支持。运行下面的命令来创建一个 NFT。
wallet create-example-nft
输出将类似于以下内容。终端打印新创建的objectId
,可以在Sui Explorer[22]上确认。一个objectId
代表一个 NFT,所以你只是铸造了一个 NFT 实例,而不是发布类似 ERC721 的合约模块。根据 NFT 的概念,这听起来更自然,不是吗?
上述命令创建了一个 ID 为0xe69e7257310c5054eda27ff474e827616c7c0b89
的对象。
你可以在创建时轻松地自定义 NFT 的名称、描述或图像:
wallet create-example-nft --url https://upload.wikimedia.org/wikipedia/commons/thumb/9/92/Sigrid_p%C3%A5_scenen_i_Oslo_Spektrum_i_2022._211328.jpg/640px-Sigrid_p%C3%A5_scenen_i_Oslo_Spektrum_i_2022._211328.jpg
其结果将类似于以下:
Successfully created an ExampleNFT:
----- Move Object (0x8433f7ca656cb16b74ec0092a9614155b848033a[1]) -----
Owner: Account Address ( 0x76705799eaef88a5378bf616fab44c96e0b8dc05 )
Version: 1
Storage Rebate: 36
Previous Transaction: XuzYR05wQOfztKyYvpwIXN1IONiGn3SnVwZByhAxluM=
----- Data -----
type: 0x2::devnet_nft::DevNetNFT
description: An NFT created by the wallet Command Line Tool
id: 0x8433f7ca656cb16b74ec0092a9614155b848033a[1]
name: Example NFT
url: https://upload.wikimedia.org/wikipedia/commons/thumb/9/92/Sigrid_p%C3%A5_scenen_i_Oslo_Spektrum_i_2022._211328.jpg/640px-Sigrid_p%C3%A5_scenen_i_Oslo_Spektrum_i_2022._211328.jpg
当通过objectId
搜索时,Sui Explorer[23]显示了 NFT 对象的详细信息。
在 Sui Explorer 上检查你的交易哈希值。终端会在上次交易下打印出交易哈希值。挖出的交易是XuzYR05wQOfztKyYvpwIXN1IONiGn3SnVwZByhAxluM
。
耐人寻味的是,你的 SUI 对象被支付了交易费用,因此,浏览器将 SUI 对象打印成上面的Mutated。你空投的 SUI 对象的数量已经减少,这意味着收取了一些 Gas。最初的 Gas 值是 50000,但现在是 39063(我已经事先发送了一些交易)。
你也许可以查看你的地址现在拥有的对象列表,如下图所示。Sui 浏览器还为你提供了可视化的数据。
wallet objects --address YOUR_ADDRESS_HERE
提示: 在实例化新对象时重复使用字节码。
Sui Move 支持重复使用已经发布的字节码,类似 CosmWasm 模块。 例如,每当你写了一行类似 use
sui::coin::Coin
,以及在 Move 代码中use 0x45aacd9ed90a5a8e211502ac3fa898a3819f23b2::module_name
。
从上述例子中可以注意到的是,在 Sui 上表达的 NFT 与以太坊完全不同。以太坊有 ERC721,这是一个非同质化代币的代币标准。铸造 NFT 需要通过部署 ERC721 合约来实例化合约。
你的数字资产只被锁定在声明它的合约里,就像之前简单说过的那样。你的 NFT 或数字资产不能自行有效地跨越合约边界。然而,在 Sui 中,每个地址都拥有原本只存储在以太坊的智能合约内的对象。Move 解放了数字资产,使其首次成为一等公民。
一个 Move 模块的不同之处在于,该合约没有自己的存储。Move 有全局存储 -- 或者说区块链状态,而是由地址来索引的。在每个地址下,都有 Move 模块和资源。全局存储用 Rust 表示,如下所示:
struct GlobalStorage {
resources: Map<address, Map<ResourceType, ResourceValue>>
modules: Map<address, Map<ModuleName, ModuleBytecode>>
}
每个地址的资源都有一个从类型到值的映射,这是一个本地的映射,很容易被地址所索引。当涉及到对应于以太坊 ERC20 的BasicCoin
模块时,有一个结构体Balance
代表每个地址的余额。
struct Coin<phantom CoinType> has store {
value: u64
}
/// Struct represents the balance of each address.
struct Balance has key {
coin: Coin // same Coin from Step 1
}
Move 区块链的状态应该大致如下:
参考:https://github.com/move-language/move/tree/main/language/documentation/tutorial
你可以在下图中注意到与以太坊状态的明显区别。在以太坊 ERC-20 合约中,每个地址的余额通常被保存在一个mapping(address => uint256)
类型的状态变量中。这个状态变量被保存在一个特定的智能合约的存储器中。
参考资料:https://github.com/move-language/move/tree/main/language/documentation/tutorial
我认识到在 web3 场景中,所有的数字资产都应该被表示为一个对象,因为它们是业务逻辑中的主要角色。 Move 语言试图通过使数字资产成为一等公民来解决这个问题,我对使用 Sui 的 Move 构建合约产生了兴趣。
在随后的文章中,我将翻阅 Sui 的文档,目前该文档在更新 Sui 的 Move 的最新发展和改进方面还比较欠缺。然后,我将创建一些 Move 教程--构建一个简单的剑例子和创建连接到前端的井字棋游戏。
原文链接: https://medium.com/dsrv/my-first-impression-of-sui-move-1-introduction-minting-a-simple-nft-f8e27941446e
[1]
MoveMoon: https://learnblockchain.cn/people/11436
[2]
Libra协会: https://www.diem.com/en-us/
[3]
清盘了该项目: https://www.bbc.com/news/technology-60156682
[4]
有大量的Move支撑的 L1区块链: https://twitter.com/sigridjin_eth/status/1544594696260833280
[5]
Aptos: https://aptoslabs.com/
[6]
Mysten Lab的Sui: https://sui.io/
[7]
Move编程语言: https://twitter.com/sigridjin_eth/status/1545026087335632896?s=20&t=DjImjLgJq6nKpMQSFMZaKw
[8]
Solidity: https://twitter.com/hashtag/Solidity?src=hashtag_click
[9]
主要特征是安全: https://hackernoon.com/hackpedia-16-solidity-hacks-vulnerabilities-their-fixes-and-real-world-examples-f3210eba5148
[10]
Sam Blackshear的演讲: https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FEG2-7bQNPv4%3Ffeature%3Doembed&display_name=YouTube&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DEG2-7bQNPv4&image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FEG2-7bQNPv4%2Fhqdefault.jpg&key=d04bfffea46d4aeda930ec88cc64b87c&type=text%2Fhtml&schema=youtube
[11]
Move编程语言旨在解决这些问题而设计的。: https://medium.com/coinmonks/overview-of-move-programming-language-a860ffd8f55d
[12]
从技术上讲,合约中用于存储、分配、函数和进程的参数或返回值的变量都可以是数字资产: https://docs.sui.io/learn/why-move
[13]
一个吸引人的测试工具: https://github.com/move-language/move/blob/main/language/documentation/book/src/unit-testing.md
[14]
三明治例子: https://github.com/MystenLabs/sui/tree/main/sui_programmability/examples/basics/sources/sandwich.move
[15]
与Mysten实验室联合创始人兼CTO、Move语言的创造者Sam Blackshear的炉边谈话: https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FlnlFtYFCYVc%3Ffeature%3Doembed&display_name=YouTube&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DlnlFtYFCYVc&image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FlnlFtYFCYVc%2Fhqdefault.jpg&key=a19fcc184b9711e1b4764040d3dc5c07&type=text%2Fhtml&schema=youtube
[16]
安装Sui binaries: https://docs.sui.io/build/install
[17]
Rust and Cargo: https://doc.rust-lang.org/cargo/getting-started/installation.html
[18]
devnet: https://docs.sui.io/explore/devnet
[19]
#devnet-faucet: https://discord.com/channels/916379725201563759/971488439931392130
[20]
here: https://discord.gg/sui
[21]
Sui Explorer: https://explorer.devnet.sui.io/
[22]
Sui Explorer: https://explorer.devnet.sui.io/objects/0xe69e7257310c5054eda27ff474e827616c7c0b89
[23]
Sui Explorer: https://explorer.devnet.sui.io/objects/0x8433f7ca656cb16b74ec0092a9614155b848033a
[24]
Wikimedia Commons: https://commons.wikimedia.org/wiki/File:Exposition_Delacroix_au_Musée_du_Louvre_à_Paris_26155962267.jpg#filelinks