分布式计数器
此示例将引导你构建一个基本的分布式计数器应用程序,涵盖了将你的 Move 代码连接到客户端应用程序的完整端到端流程。该应用程序允许你创建任何人都可以递增的计数器,但只有所有者可以重置。此示例假设你已经设置了一个带有 dApp Kit 的 React 应用程序,并具有所需的 Providers
,如 使用 Sui TypeScript SDK 的客户端应用程序 中所述。
创建 Sui 项目脚手架时,您需要使用 pnpm 包管理器。如尚未安装,可参照 pnpm 安装指南 进行安装.
如果尚未使用 Sui TypeScript SDK 遵循客户端应用程序,请在终端或控制台中运行以下命令来构建新应用程序:
pnpm create @mysten/dapp --template react-client-dapp
要快速开始,你可以使用以下 template
值自动创建此示例:
pnpm create @mysten/dapp --template react-e2e-counter
添加一个 Move 模块
你需要的第一个元素是一个用于交互的 Move 包。此示例不深入讨论 Move 代码本身,而是介绍如何部署它,并将其连接到你的 dApp。
首先,在你的项目根目录下创建一个新的 move
目录,用于存放你的 Move 代码,然后将其设置为活动目录:
mkdir move
cd move
接下来,使用 Sui 客户端 CLI 生成一个新的 Move 包。如果你已经 安装了 Sui,则 Sui CLI 已经在你的系统上。在终端或控制台中运行以下命令:
sui move new counter
这将在新的 move/counter
目录中创建一个新的空 Move 包,其中包含一个 Move.toml 文件和一个空的 sources
目录。
在 sources
下添加你的 Move 代码,创建一个新的 counter.move
文件:
module counter::counter {
use sui::transfer;
use sui::object::{Self, UID};
use sui::tx_context::{Self, TxContext};
/// A shared counter.
struct Counter has key {
id: UID,
owner: address,
value: u64
}
/// Create and share a Counter object.
public fun create(ctx: &mut TxContext) {
transfer::share_object(Counter {
id: object::new(ctx),
owner: tx_context::sender(ctx),
value: 0
})
}
/// Increment a counter by 1.
public fun increment(counter: &mut Counter) {
counter.value = counter.value + 1;
}
/// Set value (only runnable by the Counter owner)
public fun set_value(counter: &mut Counter, value: u64, ctx: &TxContext) {
assert!(counter.owner == tx_context::sender(ctx), 0);
counter.value = value;
}
}
现在你有了 Move 代码,需要将其发布。Sui TypeScript SDK 示例和应用程序模板默认使用 testnet
,因此配置你的代码以匹配要部署到的网络。
首先,通过将 Move.toml 中的 rev
从 framework/testnet
更改为 framework/devnet
来更新 Move.toml 中的 Sui 依赖项。
...
[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/devnet" }
...
接下来,配置 Sui CLI 以将 devnet
作为活动环境。如果你还没有设置 devnet
环境,可以在终端或控制台中运行以下命令:
sui client new-env --alias devnet --rpc https://fullnode.devnet.sui.io:443
运行以下命令激活 devnet
环境:
sui client switch --env devnet
现在,使用以下命令发布你的 Move 代码:
sui client publish --gas-budget 10000000 counter
有关 Sui CLI 中 client
命令的更多信息,请参阅 Sui Client CLI。
此命令的输出包含一个 packageId
值,你需要保存该值才能使用该包。
----- Object changes ----
Array [
Object {
...
},
Object {
...
},
Object {
"type": String("published"),
"packageId": String("0xcd16d38ec30a4ad609336b51f6859a6b1014c50801b47845ac7a251e436cccf7"),
"version": String("1"),
"digest": String("4bCjupBDiaANmBySAtxuAdXEvGdKW4wrya6sbmRvynEe"),
"modules": Array [
String("counter"),
],
},
]
----- Balance changes ----
将你在自己的响应中收到的 packageId
值添加到项目中的新 constants.ts
文件中:
export const COUNTER_PACKAGE_ID =
"0xcd16d38ec30a4ad609336b51f6859a6b1014c50801b47845ac7a251e436cccf7";
创建计数器
现在你已经发布了 Move 代码,你可以开始构建 UI 以使用 Move 包。 你需要一种方法来创建新的 Counter
对象。 通过创建一个新的 CreateCounter
组件来完成此操作:
export function CreateCounter(props: { onCreated: (id: string) => void }) {
return (
<div>
<button
onClick={() => {
create();
}}
>
Create Counter
</button>
</div>
);
function create() {
props.onCreated('TODO');
}
}
该组件呈现一个按钮,使用户能够创建计数器。 现在,更新你的 create
函数,以便它调用 Move 模块中的 create
函数。
为此,你需要使用适当的 moveCall
交易构造一个 TransactionBlock
,然后签署并执行可编程交易块 (PTB)。
首先,从 @mysten/sui.js
导入 TransactionBlock
,从之前创建的constants.ts文件导入“COUNTER_PACKAGE_ID”,从 @mysten/dapp-kit
导入 useSignAndExecuteTransactionBlock
。
import { useSignAndExecuteTransactionBlock } from '@mysten/dapp-kit';
import { TransactionBlock } from '@mysten/sui.js/transactions';
import { COUNTER_PACKAGE_ID } from './constants';
接下来,在组件中调用 useSignAndExecuteTransactionBlock
挂钩,它提供了一个 mutate
函数,你可以在 create
函数中使用:
export function CreateCounter(props: { onCreated: (id: string) => void }) {
const { mutate: signAndExecute } = useSignAndExecuteTransactionBlock();
return (
<div>
<button
onClick={() => {
create();
}}
>
Create Counter
</button>
</div>
);
function create() {
// TODO
}
}
最后,构建你的 TransactionBlock
:
function create() {
const txb = new TransactionBlock();
txb.moveCall({
arguments: [],
target: `${COUNTER_PACKAGE_ID}::counter::create`,
});
signAndExecute(
{
transactionBlock: txb,
options: {
// We need the effects to get the objectId of the created counter object
showEffects: true,
},
},
{
onSuccess: (tx) => {
// The first created object in this TransactionBlock should be the new Counter
const objectId = tx.effects?.created?.[0]?.reference?.objectId;
if (objectId) {
props.onCreated(objectId);
}
},
},
);
}
}
你现在有了一个可以创建新的 Counter
对象的功能组件,但如果按原样使用它,可能会遇到一些一致性问题,即你成功执行了 TransactionBlock
,但数据尚未索引以供读取 来自 RPC 节点。 为了确保 TransactionBlock 可用,你可以使用 SuiClient 的 waitForTransactionBlock 方法。
要获取 SuiClient
的实例,你可以使用 dApp Kit 中的 useSuiClient
挂钩:
import { useSignAndExecuteTransactionBlock, useSuiClient } from '@mysten/dapp-kit';
export function CreateCounter(props: { onCreated: (id: string) => void }) {
const suiClient = useSuiClient();
const { mutate: signAndExecute } = useSignAndExecuteTransactionBlock();
return <button />;
}
现在,你可以在“create”函数中使用 suiClient
来等待 TransactionBlock
被索引:
function create() {
const txb = new TransactionBlock();
txb.moveCall({
arguments: [],
target: `${COUNTER_PACKAGE_ID}::counter::create`,
});
signAndExecute(
{
transactionBlock: txb,
options: {
showEffects: true,
},
},
{
onSuccess: (tx) => {
suiClient
.waitForTransactionBlock({
digest: tx.digest,
})
.then(() => {
const objectId = tx.effects?.created?.[0]?.reference?.objectId;
if (objectId) {
props.onCreated(objectId);
}
});
},
},
);
}
设置路由
现在你的用户可以创建计数器,你需要一种路由到他们的方法。 React 应用程序中的路由可能很复杂,但此示例使其保持基本。 设置你的应用程序,以便默认呈现 CreateCounter
组件,如果你想显示特定计数器,你可以将其 ID 放入 URL 的哈希部分。
import { ConnectButton, useCurrentAccount } from '@mysten/dapp-kit';
import { isValidSuiObjectId } from '@mysten/sui.js/utils';
import { useState } from 'react';
export default function App() {
const currentAccount = useCurrentAccount();
const [counterId, setCounter] = useState(() => {
const hash = window.location.hash.slice(1);
return isValidSuiObjectId(hash) ? hash : null;
});
return (
<div>
<nav>
<ConnectButton />
</nav>
<section>
{!currentAccount ? (
'Please connect your wallet'
) : counterId ? (
<Counter id={counterId} />
) : (
<CreateCounter
onCreated={(id) => {
window.location.hash = id;
setCounter(id);
}}
/>
)}
</section>
</div>
);
}
这设置了你的应用程序以从 URL 读取哈希值,并在哈希值是有效对象 ID 时获取计数器的 ID。然后,如果有计数器 ID,则渲染 Counter
(你在下一步中定义),否则渲染来自前一步的 CreateCounter
按钮。创建计数器时,更新 URL 并设置计数器 ID。
构建计数器用户界面
对于你的计数器,你想要显示三个元素:
- 当前计数,你可以使用
getObject
RPC 方法从对象中获取。 - 一个增加按钮,调用增加 Move 函数。
- 一个重置按钮,调用
set_value
Move 函数并将值设置为0
。只有在当前用户拥有计数器时才显示。
import { useCurrentAccount, useSuiClientQuery } from '@mysten/dapp-kit';
import { SuiObjectData } from '@mysten/sui.js/client';
export function Counter({ id }: { id: string }) {
const currentAccount = useCurrentAccount();
const { data, refetch } = useSuiClientQuery('getObject', {
id,
options: {
showContent: true,
},
});
if (!data?.data) return <div>Not found</div>;
const ownedByCurrentAccount = getCounterFields(data.data)?.owner === currentAccount?.address;
return (
<div>
<div>Count: {getCounterFields(data.data)?.value}</div>
<button onClick={() => executeMoveCall('increment')}>Increment</button>
{ownedByCurrentAccount ? (
<button onClick={() => executeMoveCall('reset')}>Reset</button>
) : null}
</div>
);
function executeMoveCall(method: 'increment' | 'reset') {
// TODO
}
}
function getCounterFields(data: SuiObjectData) {
if (data.content?.dataType !== 'moveObject') {
return null;
}
return data.content.fields as { value: number; owner: string };
}
这个片段有一些新概念需要检查。它使用 useSuiClientQuery
hook 进行 getObject
RPC 调用。这将返回一个表示你的计数器的数据对象。dApp Kit 不知道你的计数器对象具有哪些字段,因此定义一个 getCounterFields
辅助函数,该函数获取计数器字段,并添加了一个类型转换,以便你可以在组件中访问期望的 value
和 owner
字段。
代码还添加了一个 executeMoveCall
函数,仍然需要实现。这与你用来创建计数器的 create
函数的工作方式相同。与你为 CreateCounter
使用回调属性不同,你可以使用 useSuiClientQuery
提供的 refetch,在执行 PTB 后重新加载你的 Counter
对象。
import {
useCurrentAccount,
useSignAndExecuteTransactionBlock,
useSuiClient,
useSuiClientQuery,
} from '@mysten/dapp-kit';
import { SuiObjectData } from '@mysten/sui.js/client';
import { TransactionBlock } from '@mysten/sui.js/transactions';
import { COUNTER_PACKAGE_ID } from './constants';
export function Counter({ id }: { id: string }) {
const currentAccount = useCurrentAccount();
const suiClient = useSuiClient();
const { mutate: signAndExecute } = useSignAndExecuteTransactionBlock();
// ...
function executeMoveCall(method: 'increment' | 'reset') {
const txb = new TransactionBlock();
if (method === 'reset') {
txb.moveCall({
arguments: [txb.object(id), txb.pure.u64(0)],
target: `${COUNTER_PACKAGE_ID}::counter::set_value`,
});
} else {
txb.moveCall({
arguments: [txb.object(id)],
target: `${COUNTER_PACKAGE_ID}::counter::increment`,
});
}
signAndExecute(
{
transactionBlock: txb,
},
{
onSuccess: (tx) => {
suiClient.waitForTransactionBlock({ digest: tx.digest }).then(() => {
refetch();
});
},
},
);
}
}
你的计数器应用程序现在已准备好进行计数。 要了解有关 dApp Kit 的更多信息,请查看 dApp Kit 文档。