Files
mpc/dashboard/app/vaults/[id]/wallets/new/page.tsx
zeekay 0e3349fdc4 feat: bridge admin dashboard, WebAuthn device management, LSS protocol selection
- Bridge admin pages: /bridge (overview), /bridge/wallets (signing wallet
  selector), /bridge/fees (fee config), /bridge/contracts (BRIDGE_ROLE mgmt)
- Bridge config API: GET/PATCH /api/v1/bridge/config, GET /bridge/networks
- BridgeConfig model in DB (org-scoped, stores wallet ID, fees, toggles)
- WebAuthn device management: /settings/devices (register/delete devices)
- Biometric approve button on transaction detail page using WebAuthn API
- Dashboard lib/webauthn.ts with startRegistration, startAuthentication
- Protocol selection in wallet creation wizard (CGGMP21, LSS, FROST)
- Protocol field on Wallet model, validation in create handler
- Wallet detail page shows protocol type
- Bridge nav link in sidebar
2025-12-27 12:12:02 -08:00

267 lines
11 KiB
TypeScript

'use client'
import { useState } from 'react'
import { useParams, useRouter } from 'next/navigation'
import Link from 'next/link'
import { Nav } from '@/components/layout/nav'
import { api } from '@/lib/api'
import type { Wallet } from '@/lib/types'
type KeyType = 'secp256k1' | 'ed25519'
type Protocol = 'cggmp21' | 'frost' | 'lss'
type Step = 'type' | 'protocol' | 'name' | 'generate' | 'result'
export default function NewWalletPage() {
const params = useParams<{ id: string }>()
const router = useRouter()
const [step, setStep] = useState<Step>('type')
const [keyType, setKeyType] = useState<KeyType>('secp256k1')
const [protocol, setProtocol] = useState<Protocol>('cggmp21')
const [walletName, setWalletName] = useState('')
const [generating, setGenerating] = useState(false)
const [error, setError] = useState('')
const [result, setResult] = useState<Wallet | null>(null)
async function handleGenerate() {
if (!walletName.trim()) return
setGenerating(true)
setError('')
setStep('generate')
try {
const wallet = await api.createWallet(params.id, {
name: walletName,
key_type: keyType,
protocol,
})
setResult(wallet)
setStep('result')
} catch (err) {
setError(err instanceof Error ? err.message : 'Key generation failed')
setStep('name')
} finally {
setGenerating(false)
}
}
return (
<>
<Nav />
<main className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
<div className="mb-8 flex items-center gap-2 text-sm text-muted-foreground">
<Link href="/vaults" className="hover:text-foreground">Vaults</Link>
<span>/</span>
<Link href={`/vaults/${params.id}`} className="hover:text-foreground">Vault</Link>
<span>/</span>
<span>New Wallet</span>
</div>
<h1 className="mb-8 text-2xl font-semibold tracking-tight">Generate New Wallet</h1>
<div className="mx-auto max-w-lg">
{/* Step indicator */}
<div className="mb-8 flex items-center justify-between">
{(['type', 'protocol', 'name', 'generate', 'result'] as Step[]).map((s, i) => (
<div key={s} className="flex items-center gap-2">
<div
className={`flex h-8 w-8 items-center justify-center rounded-full text-xs font-semibold ${
step === s
? 'bg-primary text-primary-foreground'
: i < ['type', 'protocol', 'name', 'generate', 'result'].indexOf(step)
? 'bg-emerald-500/20 text-emerald-400'
: 'bg-muted text-muted-foreground'
}`}
>
{i + 1}
</div>
{i < 4 && <div className="h-px w-8 bg-border sm:w-12" />}
</div>
))}
</div>
{/* Step: Choose key type */}
{step === 'type' && (
<div className="space-y-4">
<h2 className="text-lg font-semibold">Choose Key Type</h2>
<div className="grid gap-3 sm:grid-cols-2">
{(['secp256k1', 'ed25519'] as KeyType[]).map((t) => (
<button
key={t}
onClick={() => setKeyType(t)}
className={`rounded-lg border p-4 text-left transition-colors ${
keyType === t
? 'border-primary bg-primary/5'
: 'border-border hover:border-foreground/20'
}`}
>
<p className="font-semibold font-mono text-sm">{t}</p>
<p className="mt-1 text-xs text-muted-foreground">
{t === 'secp256k1'
? 'Bitcoin, Ethereum, Lux, XRPL'
: 'Solana, TON, Polkadot'}
</p>
</button>
))}
</div>
<div className="flex justify-end">
<button
onClick={() => setStep('protocol')}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Next
</button>
</div>
</div>
)}
{/* Step: Choose protocol */}
{step === 'protocol' && (
<div className="space-y-4">
<h2 className="text-lg font-semibold">Choose Protocol</h2>
<div className="space-y-3">
{([
{ id: 'cggmp21' as Protocol, name: 'CGGMP21', desc: 'Standard ECDSA threshold signatures. Battle-tested, recommended for most wallets.' },
{ id: 'lss' as Protocol, name: 'LSS (Dynamic Resharing)', desc: 'ECDSA with dynamic resharing — change threshold and participants without regenerating keys.' },
{ id: 'frost' as Protocol, name: 'FROST (EdDSA/Taproot)', desc: 'EdDSA and BIP-340 Taproot signatures. Use for Solana, TON, or Bitcoin Taproot.' },
]).map((p) => (
<button
key={p.id}
onClick={() => setProtocol(p.id)}
className={`w-full rounded-lg border p-4 text-left transition-colors ${
protocol === p.id
? 'border-primary bg-primary/5'
: 'border-border hover:border-foreground/20'
}`}
>
<p className="font-semibold text-sm">{p.name}</p>
<p className="mt-1 text-xs text-muted-foreground">{p.desc}</p>
</button>
))}
</div>
<div className="flex justify-between">
<button
onClick={() => setStep('type')}
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent"
>
Back
</button>
<button
onClick={() => setStep('name')}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Next
</button>
</div>
</div>
)}
{/* Step: Name wallet */}
{step === 'name' && (
<div className="space-y-4">
<h2 className="text-lg font-semibold">Name Your Wallet</h2>
<div>
<label className="mb-1.5 block text-sm font-medium text-muted-foreground">
Wallet Name
</label>
<input
type="text"
value={walletName}
onChange={(e) => setWalletName(e.target.value)}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
placeholder="Hot Wallet"
/>
</div>
{error && <p className="text-sm text-destructive">{error}</p>}
<div className="flex justify-between">
<button
onClick={() => setStep('protocol')}
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent"
>
Back
</button>
<button
onClick={handleGenerate}
disabled={!walletName.trim()}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50"
>
Generate
</button>
</div>
</div>
)}
{/* Step: Generating */}
{step === 'generate' && (
<div className="flex flex-col items-center py-12">
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-2 border-muted border-t-primary" />
<p className="text-sm text-muted-foreground">
Running distributed key generation...
</p>
<p className="mt-1 text-xs text-muted-foreground">
This may take up to 30 seconds.
</p>
</div>
)}
{/* Step: Result */}
{step === 'result' && result && (
<div className="space-y-4">
<div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-emerald-500/20 text-emerald-400">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
</div>
<h2 className="text-lg font-semibold">Wallet Created</h2>
</div>
<div className="space-y-3 rounded-lg border border-border bg-muted/50 p-4">
{result.eth_address && (
<div>
<p className="text-xs text-muted-foreground">ETH Address</p>
<p className="font-mono text-sm break-all">{result.eth_address}</p>
</div>
)}
{result.btc_address && (
<div>
<p className="text-xs text-muted-foreground">BTC Address</p>
<p className="font-mono text-sm break-all">{result.btc_address}</p>
</div>
)}
{result.sol_address && (
<div>
<p className="text-xs text-muted-foreground">SOL Address</p>
<p className="font-mono text-sm break-all">{result.sol_address}</p>
</div>
)}
{result.ecdsa_pubkey && (
<div>
<p className="text-xs text-muted-foreground">ECDSA Public Key</p>
<p className="font-mono text-xs break-all">{result.ecdsa_pubkey}</p>
</div>
)}
{result.eddsa_pubkey && (
<div>
<p className="text-xs text-muted-foreground">EdDSA Public Key</p>
<p className="font-mono text-xs break-all">{result.eddsa_pubkey}</p>
</div>
)}
</div>
<div className="flex justify-end">
<button
onClick={() => router.push(`/vaults/${params.id}`)}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Done
</button>
</div>
</div>
)}
</div>
</main>
</>
)
}