@embarkai/ui-kit
v0.3.4
Published
React UI components and hooks for EmbarkAI authentication and Account Abstraction
Downloads
2,078
Readme
@embarkai/ui-kit
React UI components and hooks for EmbarkAI - a secure, user-friendly authentication and Account Abstraction wallet solution with MPC (Multi-Party Computation) key management.
Features
- 🔐 Secure Authentication - Multiple auth methods: Email, Passkey, Telegram, Wallet connect
- 🔑 MPC Key Management - Distributed key generation with iframe isolation
- 💼 Account Abstraction - ERC-4337 compliant smart contract wallets
- 🎨 Pre-built UI Components - Ready-to-use React components with customizable themes
- ⚡ Easy Integration - Just wrap your app with
Provider
Installation
npm install @embarkai/ui-kit
# or
pnpm add @embarkai/ui-kit
# or
yarn add @embarkai/ui-kitQuick Start
The UI Kit requires @tanstack/react-query for query management & i18next + react-i18next for multilanguages support.
1. QueryClient Setup (Required)
First, create a query client:
// queryClient.ts
import { QueryClient } from '@tanstack/react-query'
export const queryClient = new QueryClient({
defaultOptions: {}
})2. I18n config (Required) & typing (Optional)
i18next lib must be initiated for both UIKIT & your app (namespace concept used to manage translations), so your app's language can be switched by UIKIT's lang selector.
There is no need to provide any translations for UIKIT ( has built-in default lang-set ), unless it's not required to expand supported languages by custom langs, but it is required to init i18next inside DApp.
NOTE If you don't need multi-language support inside your Dapp go to step 3 from here providing combineI18NResources() call with no params
First, create the following folder structure for your dapp's translations (or update existant, if needed):
example: common.json
{
"appname": "My App",
"signin": "Sign IN"
}src/
└── i18n/
├── locales/
│ ├── en/
│ ├── ├── common.json
│ ├── ├── header.json
│ ├── ├── page1.json
│ ├── ├── page2.json
│ ├── └── ...restTranslationsJsons
│ ├── /...restLocales/...
└── index.tsWhere:
locales/**/*.jsonfiles contain i18next translation maps. OPTIONAL Use dedicated files for easier translations maintainance.index.tsexports translation resources
IMPORTANT: YOUR_APP_TRANSLATION_RESOURSES must be structured with namespaces (as shown), where "app" is example namespace
import commonEn from './i18n/locales/en/common.json'
import headerEn from './i18n/locales/en/header.json'
import page1en from './locales/en/page1.json'
import page2en from './locales/en/page2.json'
export const YOUR_APP_TRANSLATION_RESOURSES = {
// language: { namespace: jsonLocale }
en: {
app: {
common: commonEn,
header: headerEn,
page1: page1en,
page2: page2en
}
}
// ...
} as constNote: If you don't need multi-language support inside your Dapp leave YOUR_APP_TRANSLATION_RESOURSES empty for now
Decalre types (usualy at src/i18next.d.ts) so t-method provides intellisence typings for your translations. Default locale is recomended to be used for typing as shown
import { YOUR_APP_TRANSLATION_RESOURSES } from './i18n'
declare module 'i18next' {
interface CustomTypeOptions {
defaultNS: 'app' //
resources: {
app: typeof YOUR_APP_TRANSLATION_RESOURSES.en.app
}
}
}3. Wrap your app with providers & init i18n
Note: Declare your specific dapp's high order useTranslation hook re-expoting namespaced react-i18next useTranslation, since now t-method is typed by your default locale.
import { useTranslation } from 'react-i18next'
export function useT() {
return useTranslation('app') // this will make t-method accordingly typed by locale
}import { combineI18NResources, LOCAL_STORAGE_I18N_KEY, Provider } from '@embarkai/ui-kit'
//
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import { YOUR_APP_TRANSLATION_RESOURSES } from './i18n'
import { queryClient } from './queryClient'
import { useT } from './useTranslation'
i18n.use(initReactI18next).init({
resources: combineI18NResources(YOUR_APP_TRANSLATION_RESOURSES), // combineI18NResources() call with no params to skip milti-lang inside dapp
lng: localStorage.getItem(LOCAL_STORAGE_I18N_KEY) || 'en', // persisted locale
fallbackLng: 'en', // default
defaultNS: 'app', // your app i18n-namespace, example: app
namespace: 'app', // your app i18n-namespace, example: app
debug: false
})
function YourDapp() {
const { t } = useT()
return (
<>
<header>
<h1>{t('common.appname')}</h1>
</header>
<main>
<span>{t('page1.title')}</span>
</main>
</>
)
}
function Root() {
return (
<QueryClientProvider client={queryClient}>
<Provider
projectId="your-project-id" // Get from EmbarkAI Dashboard
>
<YourDapp />
</Provider>
</QueryClientProvider>
)
}4. Add the ConnectWalletButton
import { ConnectWalletButton } from '@embarkai/ui-kit'
function YourDappComponent() {
const { t } = useT()
return (
<div>
<h1>{t('common.appname')}</h1>
<ConnectWalletButton label={t('common.signin')} />
</div>
)
}5. (Optional)
Custom unconnected button can be provided via ConnectButton prop. Prop consumes standart HTMLButton component and will provide required onClick to it
import { ConnectWalletButton } from '@embarkai/ui-kit'
function CustomButtonComponent(props: HTMLAttributes<HTMLButtonElement>) => {
return (<button {...props}/>)
}
function YourApp() {
const { t } = useT()
return (
<div>
<h1>My App</h1>
<ConnectWalletButton label={t('common.signin')} ConnectButton={CustomButtonComponent} />
</div>
)
}That's it! The ConnectWalletButton provides a complete authentication UI with wallet management.
Note: Don't forget to wrap your app with
QueryClientProviderfrom@tanstack/react-querybefore usingProvider, otherwise you'll get an error: "No QueryClient set, use QueryClientProvider to set one"
Configuration Options
Basic Configuration
<Provider
projectId="your-project-id" // Required
initialConfig={{
network: {
name: 'BSC Testnet',
symbol: 'BNB',
chainId: 97, // Default chain for your dApp
rpcUrl: "https://bnb-testnet.g.alchemy.com/v2/8WLaZS09KaoheJmGa4sXA",
explorerUrl: "https://testnet.bscscan.com",
testnet: true,
forceChain: false
},
}}
>Network Chain Priority:
The SDK uses the following priority for determining the active chain:
- dApp config
network.forceChain= true &&network.chainIdprovided - Your configured default chain will be forced inside your Dapp with no chain selector available - User's explicit selection - If user manually switched chains in the UI, their choice is preserved (stored in localStorage)
- dApp config
network.chainId- Preferred chainId. If no forceChain flag provided SwitchChainChange ux pops whenever user is not on preferred chainID - SDK default - BSC
Advanced Configuration
<Provider
projectId="your-project-id"
initialConfig={{
// UI customization
preferedColorMode?: 'light', // 'light' | 'dark'
ui: {
title: 'Welcome to MyApp',
subtitle: 'Sign in to continue',
dialogClassName: 'string',
useExternalIcons: false, // each social configured auth provider can be supllied by custom icons, flag forces to use provided icons via config, otherwise default icons is used
authOrder: ['email', 'passkey', 'social'],
},
// Authentication providers
email: {
enabled: true,
placeholder: 'Enter your email',
buttonText: 'Continue',
verificationTitle: 'Check your email',
},
passkey: {
enabled: true,
showCreateButton: true,
primaryButtonText: 'Sign in with Passkey',
},
social: {
enabled: true,
providers: [
{ id: 'Discord', name: 'Discord', enabled: true, comingSoon: false, icon: 'ReactIconComponent' },
{ id: 'telegram', name: 'Telegram', enabled: true, comingSoon: false, icon: 'ReactIconComponent' },
],
},
wallet: {
enabled: true,
supportedChains: [994873017, 2030232745],
requireSignature: true,
walletConnectProjectId: 'your-walletconnect-id',
},
// Features
features: {
mpcSecurity: true,
strictMode: false,
requestDeduplication: true,
kycNeeded: false,
displayNameNeeded: false,
showActiveBalanceAsFiat: false,
},
// KYC configuration (if needed)
kyc: {
provider: 'sumsub',
options: { levelName: 'basic-kyc', flowName: 'default' }
},
// Warnings
warnings: {
backupWarning: true,
emailNotConnectedWarning: true,
},
// Network configuration
// chainId sets default chain for new users (see "Network Chain Priority" above)
network: {
"name": "BSC Testnet",
"symbol": "BNB",
"chainId": 97, // Your dApp's default chain
"rpcUrl": "https\:\/\/bnb-testnet.g.alchemy.com/v2/8WLaZS09KaoheJmGa4sXA",
"explorerUrl": "https\:\/\/testnet.bscscan.com",
"testnet": true,
"forceChain": false // if true, UIKIT is immediatly forced to switch into provided chainId, chain selector via SeetingsMenu becomes anavailable
}
}}
callbacks={{
onConnecting: ({ method, provider }) => {
console.info('Connecting with:', method, provider);
},
onConnect: ({ address, session }) => {
console.info('Connected:', address, session);
},
onAccount: ({ userId, address, session, hasKeyshare }) => {
console.info('Account ready:', userId);
},
onAccountUpdate: ({ providers }) => {
console.info('Profile updated:', providers);
},
onDisconnect: ({ address, userId }) => {
console.info('Disconnected:', address);
},
onError: ({ error, message }) => {
console.error('Error:', message);
},
onWalletReady: (status) => {
console.info('Wallet ready:', status.ready);
},
onChainChange: ({ chainId, previousChainId }) => {
console.info('Chain Changed:', { previousChainId, chainId })
},
}}
>Using Hooks
Note: The
useSessionhook is based on pure Zustand store so if you're already using useSession hook please consider 2 options: 1) refactor state extarction so it uses zustand state extraction feature. 2) consider using dedicated EmbarkAI shared store values hooks:useAccountSession,useAddressetc. Otherwise you might experience excessive re-rendering issues as EmbarkAI shares its internal store and might update some state values which should not affect app render.
import { useAccountSession, useLoadingStatus } from '@embarkai/ui-kit'
function MyComponent() {
// const session = useSession(s => s.session) - with prev hook & Zustand state extraction feature, please prefer this instead:
const session = useAccountSession()
const { isSessionLoading } = useLoadingStatus()
if (isSessionLoading) return <div>Loading...</div>
if (!session) {
return <div>Not authenticated. Please connect your wallet.</div>
}
return (
<div>
<p>Welcome!</p>
<p>User ID: {session.userId}</p>
<p>Wallet Address: {session.ownerAddress}</p>
<p>Smart Account: {session.accountAddress}</p>
</div>
)
}EmbarkAI hared store values hooks
- useActiveChainId - Returns current ChainID
- useIsMobileView - Returns boolean indicating if UI is in mobile view mode
- useAccountSession - Returns current user session object with userId, addresses, and auth info
- useLoadingStatus - Returns
{ isSessionLoading, sessionStatus }for tracking authentication state - useBalance - Returns wallet balance data:
{ walletBalance, fiatBalance, cryptoRate, fiatSymbol, cryptoSymbol } - useIFrameReady - Returns boolean indicating if the MPC iframe is ready for operations
- useAddress - Returns the current user's wallet address
- useError - Returns any error that occurred during authentication or operations
- useRecoveryUserId - Returns userId for account recovery flow
- useHasServerVault - Returns boolean indicating if user has server-side keyshare backup
useSendTransaction - Send Transactions
import { useSendTransaction } from '@embarkai/ui-kit'
function TransactionExample() {
const { sendTransaction, isPending } = useSendTransaction()
const handleSend = async () => {
try {
const userOpHash = await sendTransaction({
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
value: '1000000000000000000', // 1 ETH in wei
data: '0x' // Optional contract call data
})
console.log('UserOp hash:', userOpHash)
} catch (error) {
console.error('Transaction failed:', error)
}
}
return (
<div>
<button onClick={handleSend} disabled={isPending}>
{isPending ? 'Sending...' : 'Send Transaction'}
</button>
</div>
)
}sendUserOperation - Direct Transaction Submission
For direct control without using the React hook, you can use sendUserOperation function:
import { sendUserOperation, useAccountSession } from '@embarkai/ui-kit'
function DirectTransactionExample() {
const session = useAccountSession()
const handleSend = async () => {
if (!session) {
console.error('No active session')
return
}
try {
// Send transaction directly with full control
const userOpHash = await sendUserOperation(session, {
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
value: '1000000000000000000', // 1 ETH in wei
data: '0x', // optional contract call
feeType: 'standard', // (Optional) 'economy' | 'standard' | 'fast'
})
console.log('Transaction submitted:', userOpHash)
} catch (error) {
console.error('Transaction failed:', error)
}
}
return <button onClick={handleSend}>Send Transaction</button>
}When to use:
- ✅ Use
useSendTransaction()hook for React components (automatic session management) - ✅ Use
sendUserOperation()function for custom logic, utility functions, or non-React code
deployAccount - Deploy Smart Account (Optional)
Deploy the smart account contract immediately after registration. This is optional - accounts are automatically deployed on first transaction.
Smart behavior: Automatically checks if account is already deployed and skips transaction to save gas.
import { deployAccount, useAccountSession } from '@embarkai/ui-kit'
function DeployAccountExample() {
const session = useAccountSession()
const handleDeploy = async () => {
if (!session) return
try {
// Deploy account with minimal gas cost (skips if already deployed)
const userOpHash = await deployAccount(session, 'economy')
if (userOpHash) {
console.log('Account deployed:', userOpHash)
} else {
console.log('Account already deployed, skipped')
}
} catch (error) {
console.error('Deployment failed:', error)
}
}
return <button onClick={handleDeploy}>Deploy Account</button>
}Return values:
- Returns
userOpHash(string) - if deployment was needed and executed - Returns
null- if account already deployed (saves gas)
Advanced usage:
// Force deployment even if already deployed (not recommended)
const hash = await deployAccount(session, 'economy', { force: true })Why use this?
- ✅ Pre-deploy account before first real transaction
- ✅ No user consent required (safe minimal operation)
- ✅ Cleaner UX - separate deployment from first payment
- ✅ Smart gas savings - auto-skips if already deployed
- ⚠️ Optional - accounts auto-deploy on first transaction anyway
How it works:
- Checks if smart account contract exists on-chain
- If exists: returns
nullimmediately (no gas cost) - If not exists: sends minimal UserOperation (
to=0x0, value=0, data=0x) - Factory deploys contract without user confirmation
signTypedData - Sign EIP712 Structured Messages
Sign structured data according to EIP-712 standard. This is commonly used for off-chain signatures in dApps (e.g., NFT marketplace orders, gasless transactions, permit signatures).
import { signTypedData, useAccountSession } from '@embarkai/ui-kit'
function SignatureExample() {
const session = useAccountSession()
const handleSign = async () => {
if (!session) return
try {
// Define EIP712 typed data
const signature = await signTypedData(session, {
domain: {
name: 'MyDApp', // Your dApp name (must match contract)
version: '1', // Contract version
chainId: 994,
verifyingContract: '0x...' // Your contract address (REQUIRED in production!)
},
types: {
Order: [
{ name: 'tokenIds', type: 'uint256[]' },
{ name: 'price', type: 'uint256' },
{ name: 'deadline', type: 'uint256' }
]
},
primaryType: 'Order',
message: {
tokenIds: [1n, 2n, 3n],
price: 1000000000000000000n, // 1 token in wei
deadline: BigInt(Math.floor(Date.now() / 1000) + 3600) // 1 hour from now
}
})
console.log('Signature:', signature)
// Verify signature (optional)
const { recoverTypedDataAddress } = await import('viem')
const recoveredAddress = await recoverTypedDataAddress({
domain: {
/* same domain */
},
types: {
/* same types */
},
primaryType: 'Order',
message: {
/* same message */
},
signature
})
console.log('Signer:', recoveredAddress) // Should match session.ownerAddress
} catch (error) {
console.error('Signing failed:', error)
}
}
return <button onClick={handleSign}>Sign Message</button>
}Important Notes about ERC-4337 Smart Accounts:
In Account Abstraction (ERC-4337), there are two addresses:
- Owner Address (EOA) - The address that signs messages/transactions
- Smart Account Address - The contract wallet address
⚠️ Critical: The signature is created by the owner address (EOA), NOT the smart account address!
Compatibility with existing protocols:
- ✅ Works: Protocols that verify signatures off-chain (e.g., your backend verifies the owner EOA signature)
- ⚠️ May not work: Protocols designed for EOA wallets that store and verify against
msg.senderor wallet address- Example: Uniswap Permit2, some NFT marketplaces
- These protocols expect the signer address to match the wallet address
- With smart accounts: signer = owner EOA, wallet = smart account contract
- Solution: Use ERC-1271 signature validation in your smart contracts (allows contracts to validate signatures)
Domain Configuration:
- In production, use your actual
verifyingContractaddress (not zero address!) - The
domainparameters must match exactly between frontend and smart contract - The
chainIdshould match the network you're deploying to
Technical Details:
- Shows a MetaMask-like confirmation modal with structured message preview
- All BigInt values are supported in the message
- Signature can be verified using
viem.recoverTypedDataAddress()- will return owner EOA address
When to use signTypedData:
- ✅ Custom backend signature verification (you control the verification logic)
- ✅ Gasless transactions with meta-transaction relayers
- ✅ DAO voting and governance (off-chain signatures)
- ✅ Custom smart contracts with ERC-1271 support
- ⚠️ Be cautious with protocols designed exclusively for EOA wallets
prepareUserOperation - Prepare for Backend Submission
import { prepareUserOperation, useAccountSession } from '@embarkai/ui-kit'
function BackendSubmissionExample() {
const session = useAccountSession()
const handlePrepare = async () => {
if (!session) return
// Prepare and sign UserOp without sending to bundler
const { userOp, userOpHash } = await prepareUserOperation(session, {
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
value: '1000000000000000000', // 1 ETH in wei
data: '0x',
feeType: 'standard', // (Optional) 'economy' | 'standard' | 'fast'
})
// Send to backend for validation and submission
await fetch('/api/submit-transaction', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userOp,
userOpHash,
ownerAddress: session.ownerAddress // for signature verification
})
})
}
return <button onClick={handlePrepare}>Prepare & Send to Backend</button>
}Backend example (using @embarkai/core):
import { getUserOperationReceipt, sendUserOperationRaw } from '@embarkai/core'
import { getChainConfig } from '@embarkai/core/read'
import { recoverAddress } from 'viem'
const CHAIN_ID = 2030232745
// Get chain config for bundler URL
const chainConfig = getChainConfig(CHAIN_ID)
if (!chainConfig) throw new Error('Unsupported chain')
// Receive from frontend
const { userOp, userOpHash, ownerAddress } = await request.json()
// Verify signature
const recoveredAddress = await recoverAddress({
hash: userOpHash,
signature: userOp.signature
})
if (recoveredAddress.toLowerCase() !== ownerAddress.toLowerCase()) {
throw new Error('Invalid signature')
}
// Submit to bundler - REQUIRES chainId parameter
const submittedUserOpHash = await sendUserOperationRaw(userOp, { chainId: CHAIN_ID })
// Poll for receipt to get transaction hash and status
const waitForReceipt = async (hash: string, maxAttempts = 60, delayMs = 1000) => {
for (let i = 0; i < maxAttempts; i++) {
// REQUIRES bundlerUrl parameter
const receipt = await getUserOperationReceipt(hash as `0x${string}`, chainConfig.bundlerUrl)
if (receipt) {
return {
success: receipt.success,
transactionHash: receipt.receipt?.transactionHash,
blockNumber: receipt.receipt?.blockNumber
}
}
await new Promise((resolve) => setTimeout(resolve, delayMs))
}
throw new Error('Transaction timeout')
}
const result = await waitForReceipt(submittedUserOpHash)
return {
success: result.success,
transactionHash: result.transactionHash,
blockNumber: result.blockNumber
}useOpenPage - Programmatic EmbarkAI Dialog Control
Control the EmbarkAI dialog programmatically.
import { PageKey, useOpenPage } from '@embarkai/ui-kit'
function CustomAuthButton() {
const { isOpen, open: openEmbarkAI, close } = useOpenPage()
return (
<div>
<button onClick={() => openEmbarkAI(PageKey.AUTH)}>Sign In</button>
<button onClick={() => openEmbarkAI(PageKey.RECEIVE)}>Receive CRYPTO</button>
<button onClick={close}>Close Dialog</button>
</div>
)
}ThemeToggle - Quick Theme Switcher
Pre-built theme toggle button component to use in combo with useColorMode.
import { ThemeToggle } from '@embarkai/ui-kit'
function AppHeader() {
return (
<header>
<h1>My App</h1>
<ThemeToggle />
</header>
)
}Authentication Methods
Email OTP
Configured by default, no additional setup needed
Passkey (WebAuthn)
Secure biometric authentication with device passkeys.
// Configured by default
// Users can register passkey after initial loginTelegram Mini App
Authentication via Telegram for mini apps.
// Configure via social providers:
initialConfig={{
social: {
enabled: true,
providers: [
{ id: 'telegram', name: 'Telegram', enabled: true },
],
},
}}External Wallet
...to be updated
Styling
UIKIT Dialog Window
Global UIKIT window view can be redefined via config prop - dialogClassName.
Note Providing "dialogClassName" will disable default dialog window styles
.uikit-dialog-classname {
background-color: rgba(14, 14, 14, 0.7);
backdrop-filter: blur(10px);
box-shadow:
0 0 8px rgba(14, 14, 14, 0.1),
inset 0 0 0 1px var(--embark-ui-bd);
}import 'index.css'
<Provider
...
initialConfig={{
...
ui: {
...
dialogClassName: 'uikit-dialog-classname',
...
},
...
}}/>CSSV styles & Dark/Light modes
UIKIT provides global hook for Light/Dark modes - useColorMode - use it within yor application instead of any local mode declaration.
import { useColorMode } from '@embarkai/ui-kit'
function YourAnyComponent() {
const { colorMode, setColorMode } = useColorMode()
return (
<div>
<p>Current theme: {colorMode}</p>
<button onClick={() => setColorMode('light')}>Light</button>
<button onClick={() => setColorMode('dark')}>Dark</button>
</div>
)
}UIKIT styles are possible to match with your project via css-variables set (use separate for dark & light). Values will be automatically applied according to selected colorMode. The simpliest way to customize cssv is via app index.css file:
.embarkai-ui-scope[data-current-theme-mode='light'],
.embarkai-ui-scope[data-current-theme-mode='dark'] {
/* fixed cssv */
--embark-ui-maw: 384px;
--embark-ui-pd: 12px;
--embark-ui-gap: 10px;
--embark-ui-bdrs: 20px;
--embark-ui-element-bdrs: 10px;
/** overlay */
--embark-ui-overlay: rgba(255, 255, 255, 0.8);
--embark-ui-backdrop-blur: 10px;
/** surface backgrounds */
--embark-ui-bg: #ffffff;
/** text */
--embark-ui-fg: #000000;
--embark-ui-fg-h: rgba(0, 0, 0, 0.6);
--embark-ui-fg-a: rgba(0, 0, 0, 0.4);
--embark-ui-fg-inverted: #ffffff;
--embark-ui-fg-muted: rgba(0, 0, 0, 0.6);
/** backgrounds i.e. buttons bg etc */
--embark-ui-primary: #000000;
--embark-ui-primary-h: rgba(0, 0, 0, 0.8);
--embark-ui-primary-a: rgba(0, 0, 0, 0.6);
--embark-ui-secondary: #e4e4e4;
--embark-ui-secondary-h: rgba(228, 228, 228, 0.8);
--embark-ui-secondary-a: rgba(228, 228, 228, 0.6);
/** borders */
--embark-ui-bd: #ebebeb;
--embark-ui-bd-intense: rgb(169, 169, 169);
/** shadows */
--embark-ui-shadow-c: rgba(0, 0, 0, 0.1);
/** highlight colors */
--embark-ui-info: #000000;
--embark-ui-bg-info: #e4e4e4;
--embark-ui-success: #000000;
--embark-ui-bg-success: #21ff51;
--embark-ui-warning: #000000;
--embark-ui-bg-warning: #e9fa00;
--embark-ui-error: #ffffff;
--embark-ui-bg-error: #d6204e;
}TypeScript Support
Full TypeScript support with exported types:
import type { AuthProvider, ProviderConfig, User, WalletInfo } from '@embarkai/ui-kit'Examples
Check out the /examples directory for complete working examples:
- React + Vite - Modern React setup with Vite
Security
- 🔒 MPC Key Management - Keys are split between client and server using threshold cryptography
- 🏝️ Iframe Isolation - Sensitive operations run in isolated iframe context
- 🔐 No Private Key Exposure - Private keys never exist in complete form on client
- ✅ Non-custodial - Users maintain full control of their accounts
License
MIT License - see LICENSE file for details.
