如何实现一个分布式计数器说明:该教程基于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
查询时交易摘要需替换为你发布包的摘要信息。
也可在控制台查看信息,如下图所示
╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ 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
当命令运行完毕后,将在命令行窗口提示你输入你的项目名称,之后将根据模板创建一个项目基础骨架。
本节我们直接使用官方提供的项目模板,不着重介绍如何设计前端,如何开发及交互。
当你通过以上命令创建好前端后,你将看到以下的项目骨架:
官方模板中自带了一个合约文件夹,以及前端项目。本文将采用上面的合约部分代码做示例。
我们可以看到,脚手架已经帮我们引入了基础依赖
我们通过npm install
命令进行安装依赖。
npm install
安装完成后,我们通过`npm run dev
将项目启动
npm run dev
通过访问控制台输入地址,就可以访问我们的Dapp
了。
现在我们已经将前端页面启动,也对项目有了一个大致的了解了,当时此时你通过钱包去链接项目,并创建计数器,是无法使用的,我们需要改造部分代码,才能
是Dapp
正常使用。
在模板中,constants.ts是存放我们部署的合约包的信息的配置文件,我们需要将上面我们部署的合约包信息更新到此处。
在该文件中,我们可以配置合约的主网,测试网,开发网发布的包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
运行上述命令,我们通过控制台打印的项目地址进行访问,并试验合约是否能正确调用。
页面连接钱包:
因为本地网页地址,钱包会提示不可信,我们选择continue
信任此网址即可。
连接后页面显示:
如果未创建,会按时create
按钮,可以创建计数器对象
创建后,将会将对象ID放入路由地址,之后携带对象ID即可访问创建的计数器,当然当你发布在互联网时,你可以通过对象地址让别人访问你的计数器。
http://localhost:5173/#0x10a3a3cbd7f9497d4361f4191e0fb1140fa528a912e13c5a3ac7a6d7f5bacb4b
我们可以通过increment
和reset
按钮,进行计数器的递增,以及重置计数器(当然,重置只有创建计数器对象的地址才能调用成功,这个我们在之前的合约中做过限制)
到此,已经基本完成最基础的一个分布式计数器DAPP
,希望你能有所获。
通过对本节的学习,我们学习了编写一个合约并发布,到通过前端进行调用合约方法,此实战可以使我们从0-1建造一个最基础的区中心应用
巩固我们之前学习的结构体,函数,变量,地址以及对象所有权知识点,也通过前端学习,使用dapp-kit
和sui-sdk
进行合约调用,初步入门了一个dapp
的
开发流程。
📹 课程B站账号
💻 Github仓库 https://github.com/move-cn/letsmove
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!