From sui-dev-agents
Builds Sui frontend dApps using @mysten/dapp-kit-react (React) or @mysten/dapp-kit-core (Vue, vanilla JS, other frameworks). Covers wallet connection, on-chain queries, and transaction execution.
How this skill is triggered — by the user, by Claude, or both
Slash command
/sui-dev-agents:sui-frontendThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Targets: `@mysten/sui` 2.17.0 (^2.0), `@mysten/dapp-kit-react` 2.0.3 (^2.0), `@mysten/dapp-kit-core` 1.3.2 (^1.3). Tested: 2026-05-21.
Targets: @mysten/sui 2.17.0 (^2.0), @mysten/dapp-kit-react 2.0.3 (^2.0), @mysten/dapp-kit-core 1.3.2 (^1.3). Tested: 2026-05-21.
Compatibility notes: Before installing, run npm ls @mysten/sui — if you already have @mysten/[email protected] from seal, walrus, or legacy @mysten/dapp-kit, do not add a 2.x package on top. Remediation: either upgrade those peers to versions compatible with sui 2.x, or stay fully on the 1.x line — don't mix. UI components moved to @mysten/dapp-kit-react/ui in 2.x — importing ConnectButton from the package root will fail with "is not exported".
This skill covers building browser-based Sui dApps using the dApp Kit SDK. The SDK has two packages:
@mysten/dapp-kit-react — React hooks, DAppKitProvider, and React component wrappers@mysten/dapp-kit-core — Framework-agnostic core: actions, nanostores state, and Web Components for Vue, vanilla JS, Svelte, or any other frameworkBoth packages expose the same createDAppKit factory and identical action APIs (signAndExecuteTransaction, signTransaction, signPersonalMessage, etc.). What differs is how you access reactive state and render UI: React uses hooks and provider components; other frameworks use nanostores stores and Web Components.
For PTB construction details (splitCoins, moveCall, coinWithBalance, etc.), apply the sui-ts-sdk skill alongside this one — the Transaction API is identical in browser and Node contexts.
Note: The older
@mysten/dapp-kitpackage is deprecated (JSON-RPC only, no gRPC/GraphQL support). New projects must use@mysten/dapp-kit-reactor@mysten/dapp-kit-core.
React:
npm install @mysten/dapp-kit-react @mysten/sui
# Add React Query for declarative on-chain data fetching (recommended)
npm install @tanstack/react-query
Vue / vanilla JS / other frameworks:
npm install @mysten/dapp-kit-core @mysten/sui
# For Vue reactive bindings:
npm install @nanostores/vue
| Package | Purpose |
|---|---|
@mysten/dapp-kit-react | React hooks, DAppKitProvider, React component wrappers |
@mysten/dapp-kit-core | Framework-agnostic actions, stores, Web Components |
@mysten/sui | Sui TypeScript SDK (Transaction class, gRPC client) |
@tanstack/react-query | Declarative on-chain data fetching (React only) |
@nanostores/vue | Reactive store bindings for Vue |
Not using React? Skip to the Non-React Integration section below.
The new dApp Kit uses a single createDAppKit factory instead of three nested providers. Create the instance once in a dedicated file, then wrap your app with DAppKitProvider:
// dapp-kit.ts
import { createDAppKit } from '@mysten/dapp-kit-react';
import { SuiGrpcClient } from '@mysten/sui/grpc';
const GRPC_URLS: Record<string, string> = {
testnet: 'https://fullnode.testnet.sui.io:443',
mainnet: 'https://fullnode.mainnet.sui.io:443',
};
export const dAppKit = createDAppKit({
networks: ['testnet', 'mainnet'],
defaultNetwork: 'testnet',
createClient: (network) =>
new SuiGrpcClient({ network, baseUrl: GRPC_URLS[network] }),
});
// Register the instance type for TypeScript inference in hooks
declare module '@mysten/dapp-kit-react' {
interface Register {
dAppKit: typeof dAppKit;
}
}
// @check:skip
// App.tsx
import { DAppKitProvider } from '@mysten/dapp-kit-react';
import { ConnectButton } from '@mysten/dapp-kit-react/ui';
import { dAppKit } from './dapp-kit';
export default function App() {
return (
<DAppKitProvider dAppKit={dAppKit}>
<ConnectButton />
<YourApp />
</DAppKitProvider>
);
}
The declare module augmentation is what makes useDAppKit() and other hooks return properly typed values without passing the instance explicitly.
createDAppKit accepts these key options:
// @check:skip
createDAppKit({
networks: ['testnet', 'mainnet'], // which networks your app supports
defaultNetwork: 'testnet', // starting network
createClient: (network) => // called once per network
new SuiGrpcClient({ network, baseUrl: GRPC_URLS[network] }),
autoConnect: true, // default: true — restores last wallet on reload
});
Use SuiGrpcClient here — unlike the deprecated @mysten/dapp-kit, the new package is built for gRPC. Do not pass SuiJsonRpcClient to createClient.
// @check:skip — illustrative wrong/correct contrast, not a runnable snippet
// wrong client type — belongs to the deprecated @mysten/dapp-kit era
import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc';
createClient: (network) => new SuiJsonRpcClient({ ... })
// correct
import { SuiGrpcClient } from '@mysten/sui/grpc';
createClient: (network) => new SuiGrpcClient({ network, baseUrl: GRPC_URLS[network] })
Use @mysten/dapp-kit-core when not building with React. The createDAppKit call is identical — only the import path differs:
// @check:skip
// dapp-kit.ts
import { createDAppKit } from '@mysten/dapp-kit-core'; // core, not -react
import { SuiGrpcClient } from '@mysten/sui/grpc';
const GRPC_URLS: Record<string, string> = {
testnet: 'https://fullnode.testnet.sui.io:443',
mainnet: 'https://fullnode.mainnet.sui.io:443',
};
export const dAppKit = createDAppKit({
networks: ['testnet', 'mainnet'],
defaultNetwork: 'testnet',
createClient: (network) => new SuiGrpcClient({ network, baseUrl: GRPC_URLS[network] }),
});
No declare module augmentation needed — that's React-only.
All actions on the instance work identically to the React sections below: signAndExecuteTransaction, signTransaction, signPersonalMessage, connectWallet, disconnectWallet, switchNetwork, switchAccount.
Register the web components once at your app entry point, then use them in any HTML or template:
// main.ts (app entry point)
import '@mysten/dapp-kit-core/web';
Connect Button — set instance as a DOM property (not an HTML attribute):
<mysten-dapp-kit-connect-button></mysten-dapp-kit-connect-button>
<script type="module">
import { dAppKit } from './dapp-kit.js';
document.querySelector('mysten-dapp-kit-connect-button').instance = dAppKit;
</script>
In Vue templates use property binding:
<mysten-dapp-kit-connect-button :instance="dAppKit" />
Connect Modal — for custom triggers (menu items, keyboard shortcuts, programmatic open):
<mysten-dapp-kit-connect-modal></mysten-dapp-kit-connect-modal>
<script type="module">
const modal = document.querySelector('mysten-dapp-kit-connect-modal');
modal.instance = dAppKit;
document.getElementById('open-btn').addEventListener('click', () => modal.show());
</script>
Modal events: open, opened, close, closed, cancel.
State is exposed as nanostores stores on dAppKit.stores:
| Store | Type | Description |
|---|---|---|
$connection | { wallet, account, status, isConnected, isConnecting, isReconnecting, isDisconnected } | Full connection state |
$currentNetwork | string | Active network name |
$currentClient | SuiClient | Client for the active network |
$wallets | UiWallet[] | Detected wallets |
Vanilla JS — subscribe for reactive updates:
// @check:skip
// Read current value synchronously
const connection = dAppKit.stores.$connection.get();
// Subscribe (returns an unsubscribe function — always clean up)
const unsubscribe = dAppKit.stores.$connection.subscribe((conn) => {
const el = document.getElementById('status');
if (!el) return;
if (conn.isConnected && conn.account) {
el.textContent = `${conn.wallet?.name}: ${conn.account.address}`;
} else {
el.textContent = 'Not connected';
}
});
// Unsubscribe when the view is destroyed
unsubscribe();
Vue — use @nanostores/vue for reactive template bindings:
<script setup lang="ts">
import { useStore } from '@nanostores/vue';
import { Transaction } from '@mysten/sui/transactions';
import { dAppKit } from './dapp-kit';
const connection = useStore(dAppKit.stores.$connection);
const network = useStore(dAppKit.stores.$currentNetwork);
async function handleTransfer() {
if (!connection.value.account) return;
const tx = new Transaction();
// ... build PTB ...
const result = await dAppKit.signAndExecuteTransaction({ transaction: tx });
if (result.FailedTransaction) {
throw new Error(result.FailedTransaction.status.error?.message ?? 'Transaction failed');
}
console.log('Digest:', result.Transaction.digest);
}
</script>
<template>
<mysten-dapp-kit-connect-button :instance="dAppKit" />
<div v-if="connection.account">
<p>Wallet: {{ connection.wallet?.name }}</p>
<p>Address: {{ connection.account.address }}</p>
<p>Network: {{ network }}</p>
<button @click="handleTransfer">Send Transaction</button>
</div>
<p v-else>Connect your wallet to get started</p>
</template>
Outside React there's no useCurrentClient hook. Use the store or getClient() directly:
// @check:skip
const client = dAppKit.stores.$currentClient.get();
// or equivalently:
const client = dAppKit.getClient(); // current network's client
const mainnetClient = dAppKit.getClient('mainnet'); // specific network
const connection = dAppKit.stores.$connection.get();
if (!connection.account) throw new Error('Wallet not connected');
const balance = await client.getBalance({
owner: connection.account.address,
coinType: '0x2::sui::SUI',
});
The simplest approach — renders a "Connect Wallet" button that opens a wallet selection modal. In dapp-kit-react 2.x, UI components live in the /ui subpath — importing ConnectButton from the package root will fail:
import { ConnectButton } from '@mysten/dapp-kit-react/ui';
function Header() {
return (
<header>
<ConnectButton />
</header>
);
}
ConnectButton auto-connects on page load by default (controlled by autoConnect in createDAppKit). Wallet detection happens in the browser — the component must be client-side rendered.
You can filter or sort the wallet list:
// @check:skip
<ConnectButton
modalOptions={{
filterFn: (wallet) => wallet.name !== 'ExcludedWallet',
sortFn: (a, b) => a.name.localeCompare(b.name),
}}
/>
Use useWallets to list wallets and useDAppKit for the connect/disconnect actions:
import { useWallets, useDAppKit } from '@mysten/dapp-kit-react';
function WalletMenu() {
const wallets = useWallets();
const dAppKit = useDAppKit();
return (
<div>
{wallets.map((wallet) => (
<button
key={wallet.name}
onClick={() => dAppKit.connectWallet({ wallet })}
>
{wallet.name}
</button>
))}
<button onClick={() => dAppKit.disconnectWallet()}>Disconnect</button>
</div>
);
}
useWalletConnection provides the full connection state:
import { useWalletConnection } from '@mysten/dapp-kit-react';
function ConnectionStatus() {
const { status, wallet, account } = useWalletConnection();
// status: 'disconnected' | 'connecting' | 'reconnecting' | 'connected'
if (status === 'connecting' || status === 'reconnecting') return <p>Connecting...</p>;
if (status === 'connected') return <p>Connected: {wallet?.name}</p>;
return <p>Disconnected</p>;
}
useCurrentAccount gives you the connected address; useCurrentWallet gives you the wallet object (name, icon, accounts list):
import { useCurrentAccount, useCurrentWallet } from '@mysten/dapp-kit-react';
function Profile() {
const account = useCurrentAccount();
const wallet = useCurrentWallet();
if (!account) {
return <p>No wallet connected</p>;
}
return (
<div>
<p>Wallet: {wallet?.name}</p>
<p>Address: {account.address}</p>
<p>Label: {account.label}</p>
</div>
);
}
Both return null when no wallet is connected. Always null-check before accessing their properties.
useCurrentAccount() -> UiWalletAccount | null — provides address, labeluseCurrentWallet() -> UiWallet | null — provides name, icon, accountsuseCurrentClient returns the SuiClient for the active network:
import { useCurrentClient } from '@mysten/dapp-kit-react';
function SomeComponent() {
const client = useCurrentClient();
const handleSuccess = async (digest: string) => {
// Wait for indexing before follow-up reads (see sui-ts-sdk section 11)
await client.waitForTransaction({ digest });
// All SuiGrpcClient methods are available
};
}
Do not instantiate new SuiGrpcClient(...) inside components — use useCurrentClient so it stays in sync with the active network.
Parser-breaking (v1.70+):
0x1::type_name::TypeNamevalues in structured outputs (JSON-RPC, gRPC, GraphQL) are now serialized as a plain string (e.g."0x2::sui::SUI") instead of{ "name": "0x2::sui::SUI" }. If your frontend parses these fields, expect a string at that position.
Use useDAppKit and call signAndExecuteTransaction. Build the Transaction using sui-ts-sdk PTB patterns:
import { useDAppKit, useCurrentClient, useCurrentAccount } from '@mysten/dapp-kit-react';
import { Transaction } from '@mysten/sui/transactions';
import { useState } from 'react';
function ActionButton() {
const dAppKit = useDAppKit();
const client = useCurrentClient();
const account = useCurrentAccount();
const [isPending, setIsPending] = useState(false);
const handleAction = async () => {
if (!account) return;
setIsPending(true);
try {
const tx = new Transaction();
// ... build PTB ...
const result = await dAppKit.signAndExecuteTransaction({ transaction: tx });
if (result.FailedTransaction) {
throw new Error(result.FailedTransaction.status.error?.message ?? 'Transaction failed');
}
await client.waitForTransaction({ digest: result.Transaction.digest });
} finally {
setIsPending(false);
}
};
return (
<button onClick={handleAction} disabled={!account || isPending}>
{isPending ? 'Waiting for wallet...' : 'Submit'}
</button>
);
}
Key points:
signAndExecuteTransaction is a plain async function on useDAppKit()result.FailedTransaction (failure) or result.Transaction.digest (success)waitForTransaction before re-querying stateFor querying on-chain data (React Query patterns), paginated queries, sign-without-execute, personal message signing, network switching, cache invalidation, wallet-gated UI, and the full "What dApp Kit is NOT" migration table, see:
| Mistake | Correct approach |
|---|---|
Using @mysten/dapp-kit | Deprecated — use @mysten/dapp-kit-react or @mysten/dapp-kit-core |
| Three-provider setup | Use createDAppKit + DAppKitProvider |
useSignAndExecuteTransaction hook | Use useDAppKit().signAndExecuteTransaction() |
useSuiClient | Renamed to useCurrentClient |
useSuiClientQuery | Removed — use useCurrentClient + @tanstack/react-query |
Full migration table in references/react-patterns.md.
sui-full-stack (Phase 3: Frontend development)sui-fullstack-integration (contract-frontend integration)sui-ts-sdk - PTB construction patternssui-docs-query - Query latest SDK documentationnpx claudepluginhub first-mover-tw/sui-dev-agents --plugin sui-dev-agentsProvides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.