move实战-如何实现一个分布式计数器?

  • gracecampo
  • 更新于 2024-12-10 00:00
  • 阅读 284

如何实现一个分布式计数器说明:该教程基于sui官方开发者文档,进行的是实战操作,需要读者具备一定的move语言基础。官方教程地址:https://docs.sui.io/guides/developer/app-examples/e2e-counter实战说明该实战项目涉及的知识点有:结

如何实现一个分布式计数器

🧑‍💻作者:gracecampo

说明: 该教程基于sui官方开发者文档,进行的是实战操作,需要读者具备一定的move语言基础。 官方教程地址https://docs.sui.io/guides/developer/app-examples/e2e-counter

实战说明

该实战项目涉及的知识点有: 结构体,函数,对象的所有权,PTB编程。

通过此项目,你可以构建一个,具备前后端的基础DAPP,允许任何人通过此APP进行计数器的递增,但限制只有对象的所有者可以进行重置计数器。

前置条件

move基础语法知识: 结构体 ,函数声明 ,变量声明, 对象的所有权 ,对象的能力

react前端基础知识: node , npm , react框架基础 , typescript语法

项目分析

本项目分为

合约部分: 计数器结构体 递增函数 重置函数

前端部分 钱包组件 合约调用

代码部分

创建项目结构

新建项目目录:counter_project

mkdir counter_project && cd counter_project

创建合约部分

sui move new counter_contracts

可选部分:(因为依赖为github地址,国内网速可能较慢,故将其改为gitee地址加速)

修改toml文件

[dependencies]
Sui = { git = "https://gitee.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" }

合约部分讲解

module counter_contracts::counter_contracts{
    ///声明计数器结构体:赋予结构体key的能力
    /// 结构体的元素有:
    /// id  用以在链上索引对象
    /// owner 对象的拥有者
    /// value 计数器值
    public struct Counter has key {
        id: UID,
        owner: address,
        value: u64
    }

    ///创建一个计数器:
    /// id 通过object::new(ctx)创建对象唯一索引
    /// 将owner字段赋值为函数调用地址
    /// 计数器值置为0
    public fun create(ctx: &mut TxContext) {
        transfer::share_object(Counter {
            id: object::new(ctx),
            owner: ctx.sender(),
            value: 0
        })
    }
    ///递增函数: 将计数器值+1
    public fun increment(counter: &mut Counter) {
        counter.value = counter.value + 1;
    }
    ///重置计数器值: 对计数器拥有者进行判断,如果非计数器的拥有者,则抛出异常
    public fun set_value(counter: &mut Counter, value: u64, ctx: &TxContext) {
        assert!(counter.owner == ctx.sender(), 0);
        counter.value = value;
    }
}

合约部分:

声明计数器结构体:

    public struct Counter has key {
        id: UID,
        owner: address,
        value: u64
    }

该结构体体拥有key的能力,拥有key能力的对象,必须声明一个id,此条件是拥有key的结构体的必要条件,用于在链上创建对象并用生成的id索引

我们赋予该对象id,拥有者地址 owner,以及一个记录计数器值的元素:value

声明创建结构体函数:

    public fun create(ctx: &mut TxContext) {
        transfer::share_object(Counter {
            id: object::new(ctx),
            owner: ctx.sender(),
            value: 0
        })
    }

此函数参数TxContext为一个包含了当前正在执行的交易的信息。它是由虚拟机创建的特权对象,

其中包含了 sender: 签署当前交易的用户地址。

tx_hash: 当前交易的哈希值。

epoch: 当前的纪元编号。

epoch_timestamp_ms: 纪元开始的时间戳(以毫秒为单位)。

ids_created: 在执行交易时创建的新 ID 的计数器,交易开始时总是为 0。

我们可以看到,在赋值owner地址时,我们通过调用ctx.sender(),获取了签署当前交易的用户地址。

object::new(ctx)是用于在 Sui 中创建一个新的唯一标识符(UID)的函数,它需要一个 &mut TxContext 作为参数,并返回一个新的 UID。这个函数确保生成的 UID 是唯一的,并且不能在对象被删除后重用。

value元素赋值为0

transfer::share_object 是一个用于将对象置于共享状态的函数。一旦对象被共享,它可以被任何人通过可变引用访问和修改。这个操作是不可逆的,也就是说,一旦对象被共享,它将永远保持共享状态。

我们通过transfer::share_object方法,将结构体对象置于共享状态,用以使任何人都可以操作此对象。

声明递增函数:

    public fun increment(counter: &mut Counter) {
        counter.value = counter.value + 1;
    }

此函数参数为前一个函数create创建的对象,通过counter.value = counter.value + 1,进行计数器值的递增

我们在调用时,通过传入对象ID, 函数中通过获取计数器对象的value元素,并将其原有值基础上+1,实现计数器值的递增逻辑

重置计数器值函数

    public fun set_value(counter: &mut Counter, value: u64, ctx: &TxContext) {
        assert!(counter.owner == ctx.sender(), 0);
        counter.value = value;
    }

此函数通过传入计数器对象counter,值value,以及交易上下文对象ctx,通过判断counter对象的owner地址与ctx中签署当前交易的用户地址对比,进行权限限制。

就是说虽然对象的共享的,所有人都可以通过increment函数进行修改value值,但是重置value只有owner地址才能进行修改

发布合约

sui client publish

可以通过控制台看到,我们发布的合约包信息,以及合约包的packageID

UPDATING GIT DEPENDENCY https://gitee.com/MystenLabs/sui.git
INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING counter_contracts
Successfully verified dependencies on-chain against source.
Transaction Digest: 5UQ7KuURAeMEdkQLAYq6NqM56VTLUDkkSG9WYQ8nh9Dk

控制台此信息包含了发布的交易摘要: Transaction Digest: 5UQ7KuURAeMEdkQLAYq6NqM56VTLUDkkSG9WYQ8nh9Dk

我们可以通过区块浏览器进行查看具体的信息:https://testnet.suivision.xyz/txblock/5UQ7KuURAeMEdkQLAYq6NqM56VTLUDkkSG9WYQ8nh9Dk

查询时交易摘要需替换为你发布包的摘要信息。

counter-info.png 也可在控制台查看信息,如下图所示

╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Object Changes                                                                                   │
├──────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Created Objects:                                                                                 │
│  ┌──                                                                                             │
│  │ ObjectID: 0xd717e1dc9011acb282da01c5fbc5d2dacb795c145b91e64c08fa618362893463                  │
│  │ Sender: 0x5a684e30c7760309906a4ed7b25e2d0c4bbeff74a3995a8ccbfe49be084d16d0                    │
│  │ Owner: Account Address ( 0x5a684e30c7760309906a4ed7b25e2d0c4bbeff74a3995a8ccbfe49be084d16d0 ) │
│  │ ObjectType: 0x2::package::UpgradeCap                                                          │
│  │ Version: 243530746                                                                            │
│  │ Digest: 8pWgX77LTK5CRAg651foQpMauQKq7t9Y4GESvpUf6nyR                                          │
│  └──                                                                                             │
│ Mutated Objects:                                                                                 │
│  ┌──                                                                                             │
│  │ ObjectID: 0x632d35058587efd468ac3fa5f8fbe6c17a599e51806710f910ac7aa0c3747e3e                  │
│  │ Sender: 0x5a684e30c7760309906a4ed7b25e2d0c4bbeff74a3995a8ccbfe49be084d16d0                    │
│  │ Owner: Account Address ( 0x5a684e30c7760309906a4ed7b25e2d0c4bbeff74a3995a8ccbfe49be084d16d0 ) │
│  │ ObjectType: 0x2::coin::Coin<0x2::sui::SUI>                                                    │
│  │ Version: 243530746                                                                            │
│  │ Digest: AWGTQQZ5hsvguk4GKpEvQkDdAFNmkKjHrCehpXx4n9Dx                                          │
│  └──                                                                                             │
│ Published Objects:                                                                               │
│  ┌──                                                                                             │
│  │ PackageID: 0x7fb2fd5c8ce79106206eda8759b7569588479c4057673e38113d4bf07361e23c                 │
│  │ Version: 1                                                                                    │
│  │ Digest: BbbQT37SUxtRcnvv43dyBfejyDiDTSveog7yKE9Xxdcp                                          │
│  │ Modules: counter_contracts                                                                    │
│  └──                                                                                             │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯

前端部分讲解

环境要求: 必须安装 node , npm 前端入门可以参考之前的文章:如何使用dapp-kit构建应用 官方为我们提供了一个开箱即用的模板,对于前端不太熟悉又需要看下合约成果的同学,可以直接采用官方模板,进行项目交互预览。

使用官方提供的模板脚手架创建项目:

npm create @mysten/dapp --template react-e2e-counter

如果希望可以自己设计前端以及交互逻辑,可以使用

npm create @mysten/dapp --template react-client-dapp

当命令运行完毕后,将在命令行窗口提示你输入你的项目名称,之后将根据模板创建一个项目基础骨架。

本节我们直接使用官方提供的项目模板,不着重介绍如何设计前端,如何开发及交互。

当你通过以上命令创建好前端后,你将看到以下的项目骨架:

counter.png

官方模板中自带了一个合约文件夹,以及前端项目。本文将采用上面的合约部分代码做示例。

我们可以看到,脚手架已经帮我们引入了基础依赖

counter-react.png

我们通过npm install命令进行安装依赖。

npm install

安装完成后,我们通过`npm run dev 将项目启动

npm run dev

counter-run.png

通过访问控制台输入地址,就可以访问我们的Dapp了。

counter-page.png

现在我们已经将前端页面启动,也对项目有了一个大致的了解了,当时此时你通过钱包去链接项目,并创建计数器,是无法使用的,我们需要改造部分代码,才能 是Dapp正常使用。

在模板中,constants.ts是存放我们部署的合约包的信息的配置文件,我们需要将上面我们部署的合约包信息更新到此处。

counter-constants.png 在该文件中,我们可以配置合约的主网,测试网,开发网发布的包id,当然具体环境取决于你发布在那个网络。

接下来,让我们修改在应用中网络环境的配置:networkConfig.ts

import { getFullnodeUrl } from "@mysten/sui/client";
//引入之前定义的PACKAGEID
import {
  DEVNET_COUNTER_PACKAGE_ID,
    TESTNET_COUNTER_PACKAGE_ID,
  MAINNET_COUNTER_PACKAGE_ID,
} from "./constants.ts";
import { createNetworkConfig } from "@mysten/dapp-kit";

const { networkConfig, useNetworkVariable, useNetworkVariables } =
  createNetworkConfig({
    devnet: {
        //获取官方提供的开发节点URL
      url: getFullnodeUrl("devnet"),
        //增加变量开发环境的包ID
      variables: {
        counterPackageId: DEVNET_COUNTER_PACKAGE_ID,
      },
    },
    testnet: {
        //获取官方提供的开发节点URL
      url: getFullnodeUrl("testnet"),
        //增加变量开发环境的包ID
      variables: {
        counterPackageId: TESTNET_COUNTER_PACKAGE_ID,
      },
    },
    mainnet: {
        //获取官方提供的开发节点URL
      url: getFullnodeUrl("mainnet"),
        //增加变量开发环境的包ID
      variables: {
        counterPackageId: MAINNET_COUNTER_PACKAGE_ID,
      },
    },
  });

export { useNetworkVariable, useNetworkVariables, networkConfig };

我们可以看到,此文件中配置了三个环境,并引入了各个环境的包PACKAGEID,之后我们将会在SuiClientProvider组件中使用它。

前端入口页面:main.tsx

import React from "react";
import ReactDOM from "react-dom/client";
import "@mysten/dapp-kit/dist/index.css";
import "@radix-ui/themes/styles.css";

import { SuiClientProvider, WalletProvider } from "@mysten/dapp-kit";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Theme } from "@radix-ui/themes";
import App from "./App.tsx";
import { networkConfig } from "./networkConfig.ts";

const queryClient = new QueryClient();

ReactDOM.createRoot(document.getElementById("root")!).render(
    <React.StrictMode>
        <Theme appearance="dark">
            <QueryClientProvider client={queryClient}>
                <SuiClientProvider networks={networkConfig} defaultNetwork="testnet">
                    <WalletProvider autoConnect>
                        <App />
                    </WalletProvider>
                </SuiClientProvider>
            </QueryClientProvider>
        </Theme>
    </React.StrictMode>,
);

在入口页,我们通过引入官方QueryClientProvider,SuiClientProvider,WalletProvider,用于初始化网络环境以及钱包组件。

并通过引入自定义的App组件,实现合约逻辑。

创建计数器组件:CreateCounter.tsx

此为主要与合约交互的页面:

import { Transaction } from "@mysten/sui/transactions";
import { Button, Container } from "@radix-ui/themes";
import { useSignAndExecuteTransaction, useSuiClient } from "@mysten/dapp-kit";
import { useNetworkVariable } from "./networkConfig";
import ClipLoader from "react-spinners/ClipLoader";

export function CreateCounter({
  onCreated,
}: {
  onCreated: (id: string) => void;
}) {
  const counterPackageId = useNetworkVariable("counterPackageId");
  const suiClient = useSuiClient();
  const {
    mutate: signAndExecute,
    isSuccess,
    isPending,
  } = useSignAndExecuteTransaction();

  function create() {
    const tx = new Transaction();

    tx.moveCall({
      arguments: [],
      target: `${counterPackageId}::counter_contracts::create`,
    });

    signAndExecute(
      {
        transaction: tx,
      },
      {
        onSuccess: async ({ digest }) => {
          const { effects } = await suiClient.waitForTransaction({
            digest: digest,
            options: {
              showEffects: true,
            },
          });

          onCreated(effects?.created?.[0]?.reference?.objectId!);
        },
      },
    );
  }

  return (
    <Container>
      <Button
        size="3"
        onClick={() => {
          create();
        }}
        disabled={isSuccess || isPending}
      >
        {isSuccess || isPending ? <ClipLoader size={20} /> : "Create Counter"}
      </Button>
    </Container>
  );
}

我们通过useNetworkVariable("counterPackageId"),获取当前环境中配置的PACKAGEID,这个在constants.ts定义过,并通过用户页面连接钱包后,

通过页面值传递过来。

通过const suiClient = useSuiClient(); 初始化一个sui的连接客户端,之后将通过此客户端连接,调用我们的合约方法。

该页面声明了一个create()方法,

并通过PTB编程进行合约调用:声明一个事务对象,并使用moveCall方法,调用合约的counter_contracts::create,组装事务

通过useSignAndExecuteTransaction签名并执行事务。 PTB编程可以参考:SUI中的PTB编程入门

接下来,我们通过应用主页面App.tsx进行引入 CreateCounter组件

import { ConnectButton, useCurrentAccount } from "@mysten/dapp-kit";
import { isValidSuiObjectId } from "@mysten/sui/utils";
import { Box, Container, Flex, Heading } from "@radix-ui/themes";
import { useState } from "react";
import { Counter } from "./Counter";
import { CreateCounter } from "./CreateCounter";

function App() {
  const currentAccount = useCurrentAccount();
  const [counterId, setCounter] = useState(() => {
    const hash = window.location.hash.slice(1);
    return isValidSuiObjectId(hash) ? hash : null;
  });

  return (
    <>
      <Flex
        position="sticky"
        px="4"
        py="2"
        justify="between"
        style={{
          borderBottom: "1px solid var(--gray-a2)",
        }}
      >
        <Box>
          <Heading>dApp Starter Template</Heading>
        </Box>

        <Box>
          <ConnectButton />
        </Box>
      </Flex>
      <Container>
        <Container
          mt="5"
          pt="2"
          px="4"
          style={{ background: "var(--gray-a2)", minHeight: 500 }}
        >
          {currentAccount ? (
            counterId ? (
              <Counter id={counterId} />
            ) : (
              <CreateCounter
                onCreated={(id) => {
                  window.location.hash = id;
                  setCounter(id);
                }}
              />
            )
          ) : (
            <Heading>Please connect your wallet</Heading>
          )}
        </Container>
      </Container>
    </>
  );
}

export default App;

页面中引入了CreateCounter,Counter,ConnectButton,分别是创建计数器,计数器递增及重置,钱包连接组件。

Counter组件我们就暂不介绍,和CreateCounter一样,通过PTB编程,调用的合约的递增函数以及重置函数。

到此我们页面细节大致介绍完毕,通过修改包配置以及网络配置,项目已经改造完成。

接下来,我们启动项目,就可以进行测试,是否符合我们的预期。

npm run dev

运行上述命令,我们通过控制台打印的项目地址进行访问,并试验合约是否能正确调用。

counter-run.png 页面连接钱包:

counter-wallet.png 因为本地网页地址,钱包会提示不可信,我们选择continue信任此网址即可。

wallet-connect.png

连接后页面显示:

create-counter.png 如果未创建,会按时create按钮,可以创建计数器对象

counter-object.png 创建后,将会将对象ID放入路由地址,之后携带对象ID即可访问创建的计数器,当然当你发布在互联网时,你可以通过对象地址让别人访问你的计数器。

http://localhost:5173/#0x10a3a3cbd7f9497d4361f4191e0fb1140fa528a912e13c5a3ac7a6d7f5bacb4b

我们可以通过incrementreset按钮,进行计数器的递增,以及重置计数器(当然,重置只有创建计数器对象的地址才能调用成功,这个我们在之前的合约中做过限制)

increment-reset.png 到此,已经基本完成最基础的一个分布式计数器DAPP,希望你能有所获。

总结

通过对本节的学习,我们学习了编写一个合约并发布,到通过前端进行调用合约方法,此实战可以使我们从0-1建造一个最基础的区中心应用

巩固我们之前学习的结构体,函数,变量,地址以及对象所有权知识点,也通过前端学习,使用dapp-kitsui-sdk进行合约调用,初步入门了一个dapp

开发流程。

💧  HOH水分子公众号

🌊  HOH水分子X账号

📹  课程B站账号

💻  Github仓库 https://github.com/move-cn/letsmove

  • 原创
  • 学分: 13
  • 分类: Sui
  • 标签: Move 
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
gracecampo
gracecampo
0xf349...8BF9
江湖只有他的大名,没有他的介绍。