sui-frontend — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited sui-frontend (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
This skill covers building browser-based Sui dApps using the dApp Kit SDK. The SDK has two packages:
DAppKitProvider, and React component wrappersBoth packages expose the same createDAppKit factory and identical action APIs (signAndExecuteTransaction, signTransaction, signPersonalMessage, etc.). What differs is how reactive state is accessed and UI is rendered: 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-queryVue / 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 the 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;
}
}// App.tsx
import { DAppKitProvider, ConnectButton } from '@mysten/dapp-kit-react';
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. It's the recommended approach — the alternative is passing dAppKit directly to each hook call (e.g. useWalletConnection({ dAppKit })), but that's more verbose.
createDAppKit accepts these key options:
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
// autoConnect: false, // disable if you want manual connect only
});Use `SuiGrpcClient` here — unlike the deprecated @mysten/dapp-kit, the new package is built for gRPC. Do not pass SuiJsonRpcClient to createClient.
// ❌ 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:
// 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 the application 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" />Supports modalOptions.filterFn / modalOptions.sortFn, same as the React ConnectButton.
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:
// 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>For Svelte, Solid, and other frameworks, nanostores has dedicated integrations — see the nanostores docs.
Outside React there's no useCurrentClient hook. Use the store or getClient() directly:
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',
});For live queries, subscribe to $connection and $currentNetwork and re-run when they change.
The simplest approach — renders a "Connect Wallet" button that opens a wallet selection modal:
import { ConnectButton } from '@mysten/dapp-kit-react';
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.
The wallet list can be filtered or sorted:
<ConnectButton
modalOptions={{
filterFn: (wallet) => wallet.name !== 'ExcludedWallet',
sortFn: (a, b) => a.name.localeCompare(b.name),
}}
/>dApp Kit auto-detects any wallet implementing the Sui Wallet Standard. No manual registration needed — installed wallets appear automatically.
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 returns the connected address; useCurrentWallet returns 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 — TypeScript enforces this.
useCurrentAccount() → UiWalletAccount | null — provides address, labeluseCurrentWallet() → UiWallet | null — provides name, icon, accountsuseCurrentClient returns the SuiClient for the active network. Use it for imperative async calls — inside event handlers or after a transaction:
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 §11)
await client.waitForTransaction({ digest });
// All SuiGrpcClient methods are available — see sui-ts-sdk for the full API
};
}Do not instantiate new SuiGrpcClient(...) inside components — use useCurrentClient so it stays in sync with the active network.
useSuiClientQuery no longer exists in the new package. Use useCurrentClient with @tanstack/react-query directly:
import { useCurrentClient, useCurrentAccount } from '@mysten/dapp-kit-react';
import { useQuery } from '@tanstack/react-query';
function Balance() {
const client = useCurrentClient();
const account = useCurrentAccount();
const { data, isPending, error } = useQuery({
queryKey: ['getBalance', account?.address],
queryFn: () =>
client.getBalance({
owner: account!.address,
coinType: '0x2::sui::SUI',
}),
enabled: !!account, // ✅ skip until wallet is connected
});
if (isPending) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
const sui = Number(data.totalBalance) / 1_000_000_000;
return <p>Balance: {sui.toFixed(4)} SUI</p>;
}Always wrap @tanstack/react-query usage in a QueryClientProvider:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
// Wrap your DAppKitProvider (or vice versa — order doesn't matter here)
<QueryClientProvider client={queryClient}>
<DAppKitProvider dAppKit={dAppKit}>
<App />
</DAppKitProvider>
</QueryClientProvider>Always pass `enabled: !!account` for queries that require a connected wallet. Without it, the query fires immediately with an undefined owner and errors.
Use useInfiniteQuery from @tanstack/react-query paired with useCurrentClient:
import { useCurrentClient, useCurrentAccount } from '@mysten/dapp-kit-react';
import { useInfiniteQuery } from '@tanstack/react-query';
function OwnedNFTs() {
const client = useCurrentClient();
const account = useCurrentAccount();
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
queryKey: ['getOwnedObjects', account?.address],
queryFn: ({ pageParam }) =>
client.getOwnedObjects({
owner: account!.address,
cursor: pageParam ?? null,
filter: { StructType: '0xPKG::nft::NFT' },
options: { showContent: true },
}),
initialPageParam: undefined,
getNextPageParam: (lastPage) =>
lastPage.hasNextPage ? lastPage.nextCursor : undefined,
enabled: !!account,
});
const allObjects = data?.pages.flatMap((page) => page.data) ?? [];
return (
<div>
{allObjects.map((obj) => (
<NFTCard key={obj.data?.objectId} object={obj} />
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? 'Loading...' : 'Load more'}
</button>
)}
</div>
);
}Use useDAppKit and call signAndExecuteTransaction as an async function. Build the Transaction the same way as in any other context — see sui-ts-sdk for PTB construction 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 [error, setError] = useState<string | null>(null);
const handleAction = async () => {
if (!account) return;
setIsPending(true);
setError(null);
try {
const tx = new Transaction();
// ... build PTB using sui-ts-sdk patterns ...
const result = await dAppKit.signAndExecuteTransaction({ transaction: tx });
if (result.FailedTransaction) {
throw new Error(result.FailedTransaction.status.error?.message ?? 'Transaction failed');
}
const digest = result.Transaction.digest;
await client.waitForTransaction({ digest });
// now safe to re-query updated on-chain state
} catch (e) {
setError(e instanceof Error ? e.message : 'Unknown error');
} finally {
setIsPending(false);
}
};
return (
<>
<button onClick={handleAction} disabled={!account || isPending}>
{isPending ? 'Waiting for wallet...' : 'Submit'}
</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
</>
);
}Key differences from the old API:
signAndExecuteTransaction is a plain async function on the useDAppKit() instanceresult.FailedTransaction for failure; success data is at result.Transaction.digestuseStateWhen the user's signature is needed but execution happens elsewhere (e.g., a sponsored flow where the backend attaches gas):
import { useDAppKit } from '@mysten/dapp-kit-react';
import { Transaction } from '@mysten/sui/transactions';
function SponsoredMint() {
const dAppKit = useDAppKit();
const handleSign = async () => {
const tx = new Transaction();
// ... build PTB ...
const { bytes, signature } = await dAppKit.signTransaction({ transaction: tx });
// Send bytes + signature to backend; sponsor attaches gas and executes
await fetch('/api/sponsor', {
method: 'POST',
body: JSON.stringify({ bytes, signature }),
});
};
return <button onClick={handleSign}>Mint (Gasless)</button>;
}For the server-side sponsored execution flow (attaching the sponsor's gas, collecting both signatures, submitting), see sui-ts-sdk §15.
import { useDAppKit, useCurrentAccount } from '@mysten/dapp-kit-react';
function AuthButton() {
const dAppKit = useDAppKit();
const account = useCurrentAccount();
const handleAuth = async () => {
if (!account) return;
// In production, fetch a single-use nonce from your backend to prevent replay attacks.
// Here we use a random client-side value for illustration only.
const nonce = crypto.randomUUID();
const message = new TextEncoder().encode(`Sign in to MyApp: nonce=${nonce}`);
const { bytes, signature } = await dAppKit.signPersonalMessage({ message });
// POST to backend for verification; backend should verify and invalidate the nonce.
await verifyOnServer({ address: account.address, bytes, signature });
};
return (
<button onClick={handleAuth} disabled={!account}>
Sign In
</button>
);
}The message must be a Uint8Array — use TextEncoder to convert strings. Always display message content clearly to users before signing.
Read the active network with useCurrentNetwork; switch it via useDAppKit:
import { useCurrentNetwork, useDAppKit } from '@mysten/dapp-kit-react';
function NetworkSwitcher() {
const network = useCurrentNetwork();
const dAppKit = useDAppKit();
return (
<select value={network} onChange={(e) => dAppKit.switchNetwork(e.target.value)}>
<option value="mainnet">Mainnet</option>
<option value="testnet">Testnet</option>
</select>
);
}Only networks in createDAppKit's networks array are valid targets. switchNetwork executes synchronously and does not notify the wallet — it only affects the dApp's client.
After a successful transaction, invalidate React Query caches so the UI reflects updated state. Always wait for indexing first:
import { useQueryClient } from '@tanstack/react-query';
import { useDAppKit, useCurrentClient, useCurrentAccount } from '@mysten/dapp-kit-react';
function MintButton() {
const dAppKit = useDAppKit();
const client = useCurrentClient();
const account = useCurrentAccount();
const queryClient = useQueryClient();
const handleMint = async () => {
const tx = new Transaction(); // import { Transaction } from '@mysten/sui/transactions'
// ... build PTB ...
const result = await dAppKit.signAndExecuteTransaction({ transaction: tx });
if (result.FailedTransaction) throw new Error('Mint failed');
await client.waitForTransaction({ digest: result.Transaction.digest }); // ✅ wait first
await queryClient.invalidateQueries({ queryKey: ['getBalance', account?.address] });
await queryClient.invalidateQueries({ queryKey: ['getOwnedObjects', account?.address] });
};
return <button onClick={handleMint}>Mint NFT</button>;
}// ❌ Invalidating before waitForTransaction — indexer hasn't caught up yet
const result = await dAppKit.signAndExecuteTransaction({ transaction: tx });
await queryClient.invalidateQueries(...); // stale!
await client.waitForTransaction({ digest: result.Transaction.digest });import { useCurrentAccount, ConnectButton } from '@mysten/dapp-kit-react';
function ProtectedPage() {
const account = useCurrentAccount();
if (!account) {
return (
<div>
<p>Connect your wallet to continue.</p>
<ConnectButton />
</div>
);
}
return <Dashboard address={account.address} />;
}Reusable guard:
function WalletGuard({ children }: { children: React.ReactNode }) {
const account = useCurrentAccount();
if (!account) return <ConnectButton />;
return <>{children}</>;
}| Mistake | Correct approach |
|---|---|
Using @mysten/dapp-kit in new projects | That package is deprecated; use @mysten/dapp-kit-react (React) or @mysten/dapp-kit-core (Vue, vanilla JS, other frameworks) |
Using SuiJsonRpcClient in createClient | The new dApp Kit uses SuiGrpcClient — pass it to createDAppKit's createClient |
Three-provider setup (QueryClientProvider + SuiClientProvider + WalletProvider) | Use createDAppKit + DAppKitProvider — the old provider pattern is gone |
Omitting the declare module augmentation | Without it, useDAppKit() and hooks lose TypeScript type inference |
useSignAndExecuteTransaction, useConnectWallet, useDisconnectWallet | These mutation hooks no longer exist; use useDAppKit() instance methods instead |
useSuiClient | Renamed to useCurrentClient |
useSuiClientContext | Replaced by useCurrentNetwork (read) + useDAppKit().switchNetwork() (write) |
useSuiClientQuery / useSuiClientInfiniteQuery | Removed; use useCurrentClient + useQuery/useInfiniteQuery from @tanstack/react-query |
Checking result.digest after signAndExecuteTransaction | Result is a discriminated union: use result.Transaction.digest (success) or result.FailedTransaction (failure) |
Reading account.address without null check | useCurrentAccount() returns null before connection; always guard |
enabled: !!account omitted from queries | Without it, the query fires with an undefined owner and errors immediately |
Invalidating queries before waitForTransaction | Indexer may not have processed the tx yet; always wait first |
ConnectButton in SSR without client-side guard | Wallet detection is browser-only; ensure client-side rendering for wallet components |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.