Sui Move 初体验(1) — 介绍和铸造简单的NFT

Sui Move初体验 --介绍和铸造简单的NFT

Sui Move 初体验系列文章包含:

  1. 介绍和铸造简单的NFT
  2. 建立一个简单的剑(自定义 NFT 合约)的例子
  3. 构建一个带有前端的简单井字游戏实例

让我在2019年对Web3场景感兴趣的是阅读Facebook(现在的Meta)备受期待的Libra白皮书。Libra是Facebook在区块链技术方面的新尝试,其深远的目标是为数十亿用户实现一个简单的全球货币和金融基础设施。 然而,Libra协会的参与者却受到威胁,要面对各种监管机构的高度审查。尽管Facebook重组了更名后的Libra协会,并清盘了该项目,但这家位于门罗帕克的科技巨头探索Metaverse和Web3世界的使命并没有白费。前工程师们成立了第1层区块链,继续发展他们的想法!

有大量的Move支撑的 L1区块链试图向加密货币场景介绍自己,但值得注意的是业界现在正在关注的是Aptos和Mysten Lab的Sui。他们都在协议下运行Move VM。我最近对Sui Network感兴趣,因为它声称是有吸引力的L1智能合约平台,通过横向扩展实现高吞吐量。

Move 语言

自称是Libra区块链后继者的共同点是使用Move编程语言,最初由Facebook的Libra团队开发。Solidity现在是区块链语言领域的主导者。Solidity是作为第一批利用众所周知的数据类型和数据结构来实现核心编程语言原则的区块链语言之一。

而且,虽然该语言自然都是关于资产转移的,但它没有对资产的本地支持。如果区块链语言的主要目标是数字资产活动,那么主要特征是安全。

如果你熟悉用现代语言写代码,一等公民的概念应该引起你的注意。JavaScript中的函数是一等公民的对象。所有的函数都是JavaScript中的对象,它们继承自 "Object "原型,并被赋予键值对。这些对象可以被分配给变量,并作为一个参数传递。

Sam Blackshear的演讲,他以前是Novi Research,现在是Mysten Lab的CTO

Move编程语言旨在解决这些问题而设计的。Move的原则是维护数字资产成为一等公民,它是为管理区块链上代表的数字资产而明确设计的。

从技术上讲,合约中用于存储、分配、函数和进程的参数或返回值的变量都可以是数字资产 (来自 Sui 文档)。由于Move的静态类型,编译器可以在编译期间和部署前评估大多数资源问题,这增加了智能合约的安全性。

当仔细研究EVM和Move之间的数据模型差异时,EVM的资产被编码在一个动态索引的映射里面,如所有者_地址 -> <bytes资产>。任何任意的资产都表示为HashMap中的条目,这意味着更新状态只是改变集合中的条目值。

数字资产是无法摆脱定义它们的合约的。但是,它们可以说是Web3领域中的一等公民,应该有权在语言层面上用一个专门的类型来表示。

让Move语言变得有趣的是,Move资产是任意的用户定义的类型。这些资产被Move对象所利用,这些对象可以通过一系列的交易来改变其状态。Sui文档还表示,该语言提供了内置的资源安全,允许数字资产在保持其完整性的情况下轻松地跨越合约边界流动。

该语言还具有数据可组合性的优点。制作一个包含另一个资产的新资产总是可行的。定义一个通用的包装器Z(T)也是可行的,它能够包装任何资产,提供或结合新的属性到一个被包装的对象。

更不用说,Move提供了一个吸引人的测试工具,它被整合到语言层面。如果你来自传统的Web开发背景(也被称为 "Web2"),那么这个单元测试框架被设计为可以随时随地利用。

与你所希望的相反,当下智能合约语言缺乏定义的单元测试或E2E测试的结构。同时,Move的内置测试框架在编译层面保证了类型和资产安全,就像Rust的测试框架或Java的JUnit框架一样。

让我们看看下面的三明治例子来挖掘数据的可组合性。该代码定义了结构体(资产)火腿(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);
    };
  }
}

Sui Move是一个框架,而Core Move是一种语言

与Mysten实验室联合创始人兼CTO、Move语言的创造者Sam Blackshear的炉边谈话

核心区别在于,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对象转移。

铸造一个简单的NFT

不多说了,让你的双手沾上泥土将有助于更清楚地领会Sui的Move概念。本文将建立两个例子;铸造一个简单的NFT和锻造一把剑。

首先,通过运行以下命令安装Sui binaries。在安装Sui之前,你应该先安装Rust and Cargo 工具链。

    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是Sui唯一可用的网络选项。通过输入以下命令找到你的地址。

    wallet active-address

正如在其他协议上所做的那样,你需要在Discord的#devnet-faucet上申请测试SUI代币。如果你还没有加入频道here,请求测试SUI代币,然后在Sui Explorer上检查你的交易哈希。

吸引我的一点是,使用Sui CLI铸造一个简单的NFT例子是原生支持。运行下面的命令来创建一个NFT。

    wallet create-example-nft

输出将类似于以下内容。终端打印新创建的objectId,可以在Sui Explorer上确认。一个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显示了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

数据模型 - EVM vs Move

从上述例子中可以注意到的是,在Sui上表达的NFT与以太坊完全不同。以太坊有ERC721,这是一个非同质化代币的代币标准。铸造NFT需要通过部署ERC721合约来实例化合约。

你的数字资产只被锁定在声明它的合约里,就像之前简单说过的那样。你的NFT或数字资产不能自行有效地跨越合约边界。然而,在Sui中,每个地址都拥有原本只存储在以太坊的智能合约内的对象。Move解放了数字资产,使其首次成为一等公民。

!Viva La Vida! 照片:Wikimedia Commons

一个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

本文参与区块链技术网 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 2022-09-29 17:04
  • 阅读 ( 677 )
  • 学分 ( 13 )
  • 分类:Sui

评论