sui-svelte-wallet-kit
Version:
Svelte 5 wallet kit for Sui: connect wallets, manage accounts, SuiNS, balance, sign transactions/messages
815 lines (656 loc) • 23.1 kB
Markdown
# Sui Svelte Wallet Kit
> Status: This package is under active development and is not production‑ready yet. APIs and behavior may change without notice. Use for experimentation and development only.
A Svelte 5 wallet kit for the Sui blockchain. Ship wallet connection, multi‑account management, SuiNS names, SUI balance, transaction and message signing with simple components and typed utilities.
### Features
- **Wallet connection**: Detects popular Sui wallets and connects in one click
- **Pre-built UI**: `ConnectButton` and a responsive wallet `ConnectModal`
- **Auto-connect**: Persist selection and reconnect via `localStorage`
- **Multi-account**: Read all accounts, switch by index/address
- **Passkeys**: Built-in support for biometric authentication (Face ID, Touch ID)
- **Multisig**: Integrated N-of-M multisig support (create, sign, execute proposals)
- **SuiNS & balance**: Auto-fetch SuiNS names and SUI balance (configurable)
- **Signing**: Sign and execute transactions; optional message signing
- **Svelte 5 ready**: Built with runes (`$state`, `$effect`, `$props`) and full TypeScript types
### Installation
```bash
pnpm add sui-svelte-wallet-kit
# or
npm install sui-svelte-wallet-kit
```
Peer dependency:
```bash
pnpm add svelte@^5.0.0
```
### Quick Start
```svelte
<script>
import { SuiModule, ConnectButton } from 'sui-svelte-wallet-kit';
const onConnect = () => {
console.log('Wallet connected');
};
</script>
<SuiModule {onConnect} autoConnect={true}>
<h1>My Sui dApp</h1>
<ConnectButton class="connect-btn" />
</SuiModule>
```
### Wallet Configuration
You can customize wallet display names and ordering using the `walletConfig` prop:
```svelte
<script>
import { SuiModule, ConnectButton } from 'sui-svelte-wallet-kit';
const walletConfig = {
// Custom display names for wallets
customNames: {
'Slush — A Sui wallet': 'Slush',
'Martian Sui Wallet': 'Martian',
'OKX Wallet': 'OKX',
'OneKey Wallet': 'OneKey',
'Surf Wallet': 'Surf',
'TokenPocket Wallet': 'TokenPocket'
},
// Custom ordering (wallets not listed will appear after these in alphabetical order)
ordering: [
'Slush — A Sui wallet', // Show Slush first
'OKX Wallet', // Then OKX
'Phantom', // Then Phantom
'Suiet', // Then Suiet
'Martian Sui Wallet', // Then Martian
'OneKey Wallet', // Then OneKey
'Surf Wallet', // Then Surf
'TokenPocket Wallet' // Then TokenPocket
]
};
</script>
<SuiModule {walletConfig} autoConnect={true}>
<ConnectButton />
</SuiModule>
```
**Configuration Options:**
- `customNames`: Object mapping original wallet names to custom display names
- `ordering`: Array defining the preferred order of wallets in the connect modal
**Notes:**
- Use the exact wallet names as detected by the browser (check console for available names)
- Wallets not included in `ordering` will appear after the ordered ones, sorted alphabetically
- Custom names only affect display; internal wallet identification remains unchanged
### Components
#### SuiModule
Props:
- `onConnect?: () => void`
- `autoConnect?: boolean` (default: `false`)
- `autoSuiNS?: boolean` (default: `true`)
- `autoSuiBalance?: boolean` (default: `true`)
- `walletConfig?: { customNames?: Record<string, string>; ordering?: string[] }` (optional wallet customization)
- `zkLoginGoogle?: ZkLoginGoogleConfig` (optional Enoki zkLogin config)
- `passkey?: PasskeyConfig` (optional WebAuthn passkey wallet config)
- `multisig?: MultisigConfig` (optional multisig configuration)
#### ConnectButton
Props:
- `class?: string`
- `style?: string`
- `onWalletSelection?: (payload: { wallet: any; installed: boolean; connected: boolean; alreadyConnected?: boolean }) => void`
Behavior: toggles between Connect and Disconnect based on connection state. When not connected, clicking the button opens the modal and invokes `onWalletSelection` with a payload describing the user selection so you can show a toast if the selected wallet is not installed.
#### ConnectModal
Used internally by `SuiModule`. You can access it via `getConnectModal()` and call `openAndWaitForResponse()` to let users reselect wallets while connected. For convenience, you can also use the `switchWallet(options?)` helper (see API Reference).
UI notes:
- Installed wallets are labeled "Installed" and shown first.
- A toggle button "Show other wallets" reveals the rest. The modal has a built-in max-height and scroll for long lists.
```svelte
<script>
import { switchWallet } from 'sui-svelte-wallet-kit';
// Simple programmatic switch (modal stays open until an installed wallet is picked or user cancels)
const simpleSwitch = async () => {
const res = await switchWallet();
if (res?.connected) {
console.log('Switched to', res.wallet?.name);
} else if (res?.cancelled) {
console.log('Switch cancelled');
}
};
</script>
```
Detecting not-installed wallets from the Connect button:
```svelte
<script>
import { ConnectButton } from 'sui-svelte-wallet-kit';
const onWalletSelection = (payload) => {
const picked = payload?.wallet ?? payload;
const installed = typeof payload === 'object' ? !!payload?.installed : !!picked?.installed;
if (!installed) {
// Show your toast here
console.log('[Demo] Please install:', picked?.name);
}
};
</script>
<ConnectButton class="connect-btn" {onWalletSelection} />
```
### Enoki zkLogin (Google)
Enable Google zkLogin via Enoki by passing the `zkLoginGoogle` config to `SuiModule`.
Requirements:
- Create an API key in the Enoki Portal: [Enoki Portal](https://enoki.mystenlabs.com/)
- Create a Google OAuth Client ID (typically ends with `.apps.googleusercontent.com`)
References:
- Enoki Signing In: [docs](https://docs.enoki.mystenlabs.com/ts-sdk/sign-in)
- Enoki HTTP API Specification: [docs](https://docs.enoki.mystenlabs.com/http-api/openapi)
Basic usage:
```svelte
<script>
import { SuiModule, ConnectButton } from 'sui-svelte-wallet-kit';
const zkLoginGoogle = {
apiKey: 'ENOKI_API_KEY',
googleClientId: 'GOOGLE_CLIENT_ID.apps.googleusercontent.com',
// Optional: choose network: 'mainnet' | 'testnet' | 'devnet'
network: 'testnet'
};
</script>
<SuiModule {zkLoginGoogle} autoConnect={true}>
<ConnectButton />
</SuiModule>
```
Notes:
- The "Sign in with Google" entry appears in the Connect modal only when `zkLoginGoogle` is provided and passes basic validation.
- The SDK probes your API key once via `GET /v1/app` for an early validity check.
- You can change networks by setting `zkLoginGoogle.network`. Default is: `mainnet`
- Check browser console logs for detailed hints emitted by `SuiModule`.
### Passkey Wallet (WebAuthn)
Enable passwordless wallet authentication using device biometrics (Face ID, Touch ID, Windows Hello).
```svelte
<script>
import { SuiModule, ConnectButton } from 'sui-svelte-wallet-kit';
const passkey = {
rpId: window.location.hostname, // Must match your domain
rpName: 'My Sui App' // Display name in passkey prompts
};
</script>
<SuiModule {passkey} autoConnect={true}>
<ConnectButton />
</SuiModule>
```
Configuration options:
| Option | Type | Required | Description |
| ------------------------- | ------------------------------ | -------- | -------------------------------------- |
| `rpId` | string | Yes | Domain name (must match actual domain) |
| `rpName` | string | Yes | Display name in passkey prompts |
| `authenticatorAttachment` | 'platform' \| 'cross-platform' | No | Device biometrics or security keys |
| `icon` | data URL | No | Custom wallet icon (base64 svg/png) |
For detailed documentation including recovery flows, technical details, and troubleshooting, see [docs/PASSKEY_INTEGRATION.md](docs/PASSKEY_INTEGRATION.md).
### Multisig Support
Built-in support for **N-of-M** multisig transactions. Define static signers (DAO mode) or let users create their own multisig configurations (Dynamic mode). Supports mixing standard wallets, zkLogin, and Passkeys as signers.
```svelte
<script>
const multisigConfig = {
mode: 'dynamic', // or 'preconfigured'
network: 'testnet',
storageKey: 'my-multisig-config'
};
</script>
<SuiModule {multisigConfig}>
<!-- Your App -->
</SuiModule>
```
See the full guide: [docs/MULTISIG_GUIDE.md](docs/MULTISIG_GUIDE.md).
### API Reference
Exports from `sui-svelte-wallet-kit`:
- Components: `SuiModule`, `ConnectButton`, `ConnectModal`
- Connection: `connectWithModal(onSelection?)`, `getConnectModal`, `connect(wallet)`, `disconnect`, `switchWallet(options?)`
- Signing: `signAndExecuteTransaction(transaction)`, `signMessage(message)`, `canSignMessage()`
- Wallet info: `useCurrentWallet()`, `lastWalletSelection`
- Accounts: `useCurrentAccount()`, `useAccounts()`, `activeAccountIndex`, `switchAccount(selector)`, `setAccountLabel(name)`, `accountLoading`
- SuiNS: `suiNames`, `suiNamesLoading`, `suiNamesByAddress`
- Balance: `suiBalance`, `suiBalanceLoading`, `suiBalanceByAddress`, `refreshSuiBalance(address, options?)`
- SuiClient: `useSuiClient()`
- Discovery: `walletAdapters`, `availableWallets`
- Enoki/zkLogin: `isZkLoginWallet()`, `getZkLoginInfo()`
- Multisig: `useMultisig()`
Examples:
```svelte
<script>
import {
useCurrentAccount,
useSuiClient,
accountLoading,
connectWithModal,
switchWallet,
disconnect,
signAndExecuteTransaction,
signMessage,
canSignMessage,
switchAccount,
suiBalance,
refreshSuiBalance,
isZkLoginWallet,
getZkLoginInfo
} from 'sui-svelte-wallet-kit';
import { Transaction } from '@mysten/sui/transactions';
// Use hooks for reactive state
let account = $derived(useCurrentAccount());
let suiClient = $derived(useSuiClient());
$effect(async () => {
if (account && isZkLoginWallet()) {
const info = await getZkLoginInfo();
console.log('zkLogin session/metadata:', info);
}
});
</script>
```
### Styling
Override the modal by targeting its classes:
```css
:global(.modal-overlay) {
background: rgba(0, 0, 0, 0.8);
}
:global(.modal-content) {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 16px;
}
:global(.wallet-button) {
background: #2a2a2a;
border: 1px solid #444;
color: white;
}
:global(.wallet-button:hover) {
border-color: #667eea;
background: #333;
}
```
### Supported Wallets
Built on `@suiet/wallet-sdk` and Wallet Radar. Detects popular Sui wallets such as Slush, Suiet, Sui Wallet, Ethos, Surf, Glass, and others available in the browser.
### TypeScript
The package includes full TypeScript support with comprehensive type definitions for all components, utilities, and reactive stores.
#### Using Types in Your Project
Import types directly from the package:
```typescript
import type {
SuiAccount,
SuiWallet,
WalletConfig,
ZkLoginGoogleConfig,
SignMessageResult,
WalletSelectionPayload,
ConnectionResult,
SwitchWalletOptions
} from 'sui-svelte-wallet-kit';
```
#### Type-safe Component Usage
```svelte
<script lang="ts">
import { SuiModule, ConnectButton } from 'sui-svelte-wallet-kit';
import type {
WalletConfig,
ZkLoginGoogleConfig,
WalletSelectionPayload
} from 'sui-svelte-wallet-kit';
// Type-safe configuration
const walletConfig: WalletConfig = {
ordering: ['Slush — A Sui wallet', 'OKX Wallet'],
customNames: {
'Slush — A Sui wallet': 'Slush'
}
};
const zkLoginGoogle: ZkLoginGoogleConfig = {
apiKey: 'your-api-key',
googleClientId: 'your-client-id.apps.googleusercontent.com',
network: 'testnet'
};
// Type-safe callback
const onWalletSelection = (payload: WalletSelectionPayload): void => {
console.log('Selected:', payload.wallet.name);
console.log('Installed:', payload.installed);
};
const onConnect = (): void => {
console.log('Connected!');
};
</script>
<SuiModule {walletConfig} {zkLoginGoogle} {onConnect} autoConnect={true}>
<ConnectButton {onWalletSelection} />
</SuiModule>
```
#### Type-safe State and Functions
All reactive stores and functions are fully typed:
```svelte
<script lang="ts">
import {
useCurrentAccount,
useSuiClient,
useCurrentWallet,
suiBalance,
switchAccount,
signMessage,
refreshSuiBalance
} from 'sui-svelte-wallet-kit';
import type { SuiAccount, SuiWallet, SignMessageResult, SuiClient } from 'sui-svelte-wallet-kit';
// Use hooks for reactive state
let account = $derived(useCurrentAccount());
let client = $derived(useSuiClient());
let currentWallet = $derived(useCurrentWallet());
// Reactive values are type-safe
$effect(() => {
const a: SuiAccount | undefined = account;
const c: SuiClient = client;
const w: SuiWallet | undefined = currentWallet;
const balance: string | null = suiBalance.value;
console.log('Account:', a?.address);
console.log('Client network:', c);
console.log('Wallet:', w?.name);
console.log('Balance:', balance);
});
const handleSwitchAccount = (index: number): boolean => switchAccount(index);
const handleSignMessage = async (message: string): Promise<void> => {
try {
const result: SignMessageResult = await signMessage(message);
console.log('Signature:', result.signature);
console.log('Message bytes:', result.messageBytes);
} catch (error) {
console.error('Signing failed:', error);
}
};
const handleRefreshBalance = async (address: string): Promise<void> => {
const balance: string | null = await refreshSuiBalance(address, { force: true, ttlMs: 5000 });
console.log('Refreshed balance:', balance);
};
const fetchBalance = async (): Promise<void> => {
if (!account) return;
const balance = await client.getBalance({ owner: account.address });
console.log('Balance:', balance);
};
</script>
```
#### Type-safe Wallet Switching
```svelte
<script lang="ts">
import { switchWallet } from 'sui-svelte-wallet-kit';
import type { SwitchWalletOptions, ConnectionResult, SuiWallet } from 'sui-svelte-wallet-kit';
const handleSwitchWallet = async (): Promise<void> => {
const options: SwitchWalletOptions = {
onSelection: ({ wallet, installed }) => {
console.log('Selected:', wallet.name, 'Installed:', installed);
},
shouldConnect: ({ selectedWallet, currentWallet }) => {
// Skip if selecting same wallet
return selectedWallet?.name !== currentWallet?.name;
},
onBeforeDisconnect: (current, selected) => {
console.log('Switching from', current?.name, 'to', selected?.name);
},
onConnected: (wallet) => {
console.log('Connected to:', wallet?.name);
},
onCancel: () => {
console.log('Switch cancelled');
}
};
const result: ConnectionResult = await switchWallet(options);
if (result.connected) {
console.log('Successfully switched to:', result.wallet?.name);
} else if (result.cancelled) {
console.log('User cancelled the switch');
}
};
</script>
```
#### Available Types
**Core Types:**
- `SuiAccount` - Account information with address, chains, label
- `SuiWallet` - Wallet information with name, icon, adapter
- `WalletConfig` - Configuration for wallet ordering and custom names
- `ZkLoginGoogleConfig` - Configuration for Enoki zkLogin with Google
- `ZkLoginInfo` - zkLogin session and metadata
- `PasskeyConfig` - Configuration for WebAuthn passkey wallet
**Result Types:**
- `SignMessageResult` - Message signature result with signature and messageBytes
- `ConnectionResult` - Connection/switch result with wallet info and status
- `WalletSelectionPayload` - Wallet selection payload with wallet and installed status
**Options Types:**
- `SwitchWalletOptions` - Options for `switchWallet()` with callbacks
- `RefreshBalanceOptions` - Options for `refreshSuiBalance(address, options?)` with force and TTL
**Store Types:**
- `ReadableStore<T>` - Base reactive store with `value` getter
- `AccountStore` - Account store with setAccount/removeAccount methods
- `SuiNamesStore` - SuiNS names store with clear method
- `LastWalletSelectionStore` - Last selection store with clear method
#### IntelliSense Benefits
With TypeScript enabled, you get:
- ✅ **Autocomplete** for all props, functions, and store properties
- ✅ **Type checking** catches errors at compile time
- ✅ **Inline documentation** via hover tooltips
- ✅ **Refactoring support** with safe renames across your codebase
#### Configuration
Ensure your `tsconfig.json` includes:
```json
{
"compilerOptions": {
"moduleResolution": "bundler",
"strict": true
}
}
```
### Development
```bash
# Install deps
pnpm i
# Build the package
pnpm run prepack
# Lint package exports
pnpm run lint:package
# Optional sanity check before publishing
./scripts/publish-check.sh
```
### License
MIT — see `LICENSE`.
### Links
- Repository: `https://github.com/teededung/sui-svelte-wallet-kit`
- Issues: `https://github.com/teededung/sui-svelte-wallet-kit/issues`
### More Usage Examples
Below are a few practical examples adapted from the demo page (`src/routes/+page.svelte`).
Connect, switch, disconnect with UX callbacks:
```svelte
<script>
import {
ConnectButton,
connectWithModal,
switchWallet,
disconnect,
useCurrentWallet
} from 'sui-svelte-wallet-kit';
const onWalletSelection = (payload) => {
const picked = payload?.wallet ?? payload;
const installed = typeof payload === 'object' ? !!payload?.installed : !!picked?.installed;
if (!installed) alert('Please install the wallet: ' + picked?.name);
};
const onSwitchWallet = async () => {
const currentWallet = $derived(useCurrentWallet());
await switchWallet({
onSelection: onWalletSelection,
shouldConnect: ({ selectedWallet }) => {
// Example: skip reconnecting to the same wallet if it lacks native account picker
if (currentWallet?.name && selectedWallet?.name === currentWallet.name) return false;
return true;
}
});
};
</script>
<ConnectButton class="connect-btn" {onWalletSelection} />
<button onclick={onSwitchWallet}>Switch Wallet</button>
<button onclick={disconnect}>Disconnect</button>
```
Show account info, SuiNS, balance, and refresh balance:
```svelte
<script>
import {
useCurrentAccount,
useAccounts,
suiNames,
suiNamesLoading,
suiBalance,
suiBalanceLoading,
refreshSuiBalance
} from 'sui-svelte-wallet-kit';
let account = $derived(useCurrentAccount());
const formatSui = (balance) => {
try {
const n = BigInt(balance);
const whole = n / 1000000000n;
const frac = n % 1000000000n;
const fracStr = frac.toString().padStart(9, '0').replace(/0+$/, '');
return fracStr ? `${whole}.${fracStr}` : whole.toString();
} catch (_) {
return balance ?? '0';
}
};
</script>
{#if account}
<p><strong>Address:</strong> {account.address}</p>
<p><strong>Chains:</strong> {account.chains?.join(', ') || 'N/A'}</p>
{#if suiNamesLoading.value}
<p><strong>SuiNS Names:</strong> Loading...</p>
{:else}
<p>
<strong>SuiNS Names:</strong>
{Array.isArray(suiNames.value) && suiNames.value.length > 0
? suiNames.value.join(', ')
: 'N/A'}
</p>
{/if}
<p>
<strong>SUI Balance:</strong>
{#if suiBalanceLoading.value}
Loading...
{:else}
{formatSui(suiBalance.value || '0')} SUI
{/if}
</p>
<button
onclick={() => refreshSuiBalance(account.address)}
disabled={!account || suiBalanceLoading.value}
>
Refresh Balance
</button>
{/if}
```
Sign and execute a simple transaction:
```svelte
<script>
import { signAndExecuteTransaction, useCurrentAccount } from 'sui-svelte-wallet-kit';
import { Transaction } from '@mysten/sui/transactions';
let account = $derived(useCurrentAccount());
let isLoading = false;
let transactionResult = null;
let error = null;
const testTransaction = async () => {
if (!account) {
error = 'Please connect your wallet first';
return;
}
isLoading = true;
error = null;
transactionResult = null;
try {
const tx = new Transaction();
// Example: transfer 0 SUI to self (no-op); replace with your own commands
tx.transferObjects([tx.splitCoins(tx.gas, [0])], account.address);
transactionResult = await signAndExecuteTransaction(tx);
} catch (err) {
error = err?.message || 'Transaction failed';
} finally {
isLoading = false;
}
};
</script>
<button onclick={testTransaction} disabled={isLoading}>
{isLoading ? 'Signing Transaction...' : 'Test Transaction (0 SUI transfer)'}
</button>
{#if error}
<p style="color:#fca5a5">{error}</p>
{/if}
{#if transactionResult}
<pre>{JSON.stringify(transactionResult, null, 2)}</pre>
{/if}
```
Sign a message (works with wallets supporting `sui:signMessage` or Enoki `sui:signPersonalMessage`):
```svelte
<script>
import { canSignMessage, signMessage, useCurrentAccount } from 'sui-svelte-wallet-kit';
let account = $derived(useCurrentAccount());
let message = 'Hello, Sui blockchain!';
let signatureResult = null;
let isSigningMessage = false;
let error = null;
const testSignMessage = async () => {
if (!account) {
error = 'Please connect your wallet first';
return;
}
isSigningMessage = true;
error = null;
signatureResult = null;
try {
signatureResult = await signMessage(message);
} catch (err) {
error = err?.message || 'Message signing failed';
} finally {
isSigningMessage = false;
}
};
</script>
{#if account}
{#if canSignMessage()}
<input bind:value={message} placeholder="Enter message to sign" />
<button onclick={testSignMessage} disabled={isSigningMessage}>
{isSigningMessage ? 'Signing Message...' : 'Sign Message'}
</button>
{:else}
<p>Current wallet does not support message signing.</p>
{/if}
{:else}
<p>Connect your wallet to sign messages</p>
{/if}
{#if signatureResult}
<p><strong>Signature:</strong> <code>{signatureResult.signature}</code></p>
{/if}
```
Show zkLogin (Enoki) session/metadata when connected via Google:
```svelte
<script>
import { getZkLoginInfo, useCurrentAccount } from 'sui-svelte-wallet-kit';
let account = $derived(useCurrentAccount());
let zkInfo = null;
$effect(async () => {
zkInfo = account ? await getZkLoginInfo() : null;
});
</script>
{#if zkInfo}
<div>
<strong>zkLogin (Enoki)</strong>
{#if zkInfo.metadata}
<p><strong>Provider:</strong> {zkInfo.metadata?.provider || 'N/A'}</p>
{/if}
{#if zkInfo.session}
<p><strong>Session:</strong></p>
<pre>{JSON.stringify(zkInfo.session, null, 2)}</pre>
{:else}
<p>No zkLogin session info.</p>
{/if}
</div>
{/if}
```
Account switching by index:
```svelte
<script>
import { accounts, activeAccountIndex, switchAccount } from 'sui-svelte-wallet-kit';
let selectedAccountIndex = -1;
$effect(() => {
selectedAccountIndex = activeAccountIndex.value;
});
const onAccountChange = () => switchAccount(Number(selectedAccountIndex));
</script>
{#if accounts.value.length > 1}
<select bind:value={selectedAccountIndex} onchange={onAccountChange}>
{#each accounts.value as acc, i}
<option value={i} selected={i === activeAccountIndex.value}>
#{i + 1} — {acc.address}
</option>
{/each}
</select>
{/if}
```