Files
zeekay 6b38df60a4 feat: MPC wallet dashboard - full-stack implementation
Complete MPC wallet management platform rivaling Fireblocks/Utila/Fordefi:

Backend (Go):
- REST API with 60+ endpoints on chi router (pkg/api/)
- PostgreSQL schema with 13 tables (pkg/db/)
- JWT + MFA auth, bcrypt passwords, API key management
- Policy engine (deny/approve/require_approval)
- Transaction signing flow with approval workflow
- Subscription scheduler for recurring payments
- Payment requests with public pay pages
- Safe + ERC-4337 smart wallet integration (pkg/smart/)
- Rate limiting, webhook delivery (HMAC-SHA256), audit logging
- 27 passing tests (auth, policy engine, middleware)

Frontend (Next.js 14):
- 27 routes with monochrome dark theme (Tailwind + Geist fonts)
- Full API client with JWT auto-refresh
- React Query hooks for all resources
- Pages: dashboard, vaults, wallets, transactions, policies,
  whitelist, team, settings, API keys, webhooks, audit,
  subscriptions, payments, smart wallets

Infrastructure:
- Dockerfiles for API (Go) and dashboard (Next.js standalone)
- compose.yml for local development (PostgreSQL + API + dashboard)
- K8s manifests for mpc.lux.network + cloud.lux.network
- GitHub Actions deploy workflow (build → push GHCR → kubectl apply)
- TLS via cert-manager, Traefik IngressRoute for both domains
2025-12-27 13:11:38 -08:00

378 lines
13 KiB
TypeScript

'use client'
import { useState } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { DeployWizard } from '@/components/smart-wallets/deploy-wizard'
type WalletType = 'safe' | 'erc4337'
type DeployStep = 'type' | 'config' | 'advanced' | 'deploy'
interface DeployConfig {
type: WalletType
chain: string
chainId: number
owners: string[]
threshold: number
factoryAddress: string
salt: string
}
const chains = [
{ name: 'Ethereum', chainId: 1 },
{ name: 'Lux C-Chain', chainId: 96369 },
{ name: 'Lux Testnet', chainId: 96368 },
{ name: 'Zoo', chainId: 200200 },
{ name: 'Hanzo', chainId: 36963 },
]
export default function DeploySmartWalletPage() {
const params = useParams<{ id: string }>()
const router = useRouter()
const [step, setStep] = useState<DeployStep>('type')
const [config, setConfig] = useState<DeployConfig>({
type: 'safe',
chain: 'Ethereum',
chainId: 1,
owners: [''],
threshold: 1,
factoryAddress: '',
salt: '',
})
const [deploying, setDeploying] = useState(false)
const [deployedAddress, setDeployedAddress] = useState('')
const [error, setError] = useState('')
function addOwner() {
setConfig((c) => ({ ...c, owners: [...c.owners, ''] }))
}
function updateOwner(index: number, value: string) {
setConfig((c) => {
const owners = [...c.owners]
owners[index] = value
return { ...c, owners }
})
}
function removeOwner(index: number) {
setConfig((c) => ({
...c,
owners: c.owners.filter((_, i) => i !== index),
}))
}
async function handleDeploy() {
setDeploying(true)
setError('')
try {
// TODO: call deploy API
await new Promise((resolve) => setTimeout(resolve, 5000))
setDeployedAddress('0x9876...fedc')
} catch (err) {
setError(err instanceof Error ? err.message : 'Deployment failed')
} finally {
setDeploying(false)
}
}
return (
<div className="mx-auto max-w-2xl space-y-8 px-4 py-8">
<div>
<h1 className="text-2xl font-semibold tracking-tight">
Deploy Smart Wallet
</h1>
<p className="mt-1 text-sm text-muted-foreground">
Deploy a Safe or ERC-4337 wallet backed by MPC key{' '}
<span className="font-mono">{params.id}</span>
</p>
</div>
{/* Step indicator */}
<DeployWizard currentStep={step} />
{/* Step 1: Choose type */}
{step === 'type' && (
<section className="space-y-4">
<h2 className="text-sm font-medium uppercase tracking-wider text-muted-foreground">
Step 1: Choose Wallet Type
</h2>
<div className="grid grid-cols-2 gap-4">
{(['safe', 'erc4337'] as const).map((t) => (
<button
key={t}
onClick={() => setConfig((c) => ({ ...c, type: t }))}
className={`rounded-lg border p-6 text-left transition-colors ${
config.type === t
? 'border-primary bg-primary/5'
: 'border-border hover:border-muted-foreground'
}`}
>
<p className="text-sm font-medium">
{t === 'safe' ? 'Safe (Gnosis Safe)' : 'ERC-4337 Account'}
</p>
<p className="mt-1 text-xs text-muted-foreground">
{t === 'safe'
? 'Multi-sig smart wallet with on-chain approvals'
: 'Account abstraction with bundler and paymaster support'}
</p>
</button>
))}
</div>
<button
onClick={() => setStep('config')}
className="w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
>
Next
</button>
</section>
)}
{/* Step 2: Chain, owners, threshold */}
{step === 'config' && (
<section className="space-y-6">
<h2 className="text-sm font-medium uppercase tracking-wider text-muted-foreground">
Step 2: Configuration
</h2>
<div className="space-y-2">
<label className="text-sm font-medium" htmlFor="chain">
Chain
</label>
<select
id="chain"
value={config.chainId}
onChange={(e) => {
const chain = chains.find(
(c) => c.chainId === Number(e.target.value)
)
if (chain) {
setConfig((c) => ({
...c,
chain: chain.name,
chainId: chain.chainId,
}))
}
}}
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"
>
{chains.map((c) => (
<option key={c.chainId} value={c.chainId}>
{c.name} ({c.chainId})
</option>
))}
</select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Owners</label>
{config.owners.map((owner, i) => (
<div key={i} className="flex gap-2">
<input
type="text"
value={owner}
onChange={(e) => updateOwner(i, e.target.value)}
placeholder="0x..."
className="flex-1 rounded-md border border-input bg-background px-3 py-2 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-ring"
/>
{config.owners.length > 1 && (
<button
onClick={() => removeOwner(i)}
className="rounded-md border border-border px-3 py-2 text-sm text-muted-foreground hover:bg-accent"
>
Remove
</button>
)}
</div>
))}
<button
onClick={addOwner}
className="text-sm font-medium text-primary hover:underline"
>
+ Add Owner
</button>
</div>
<div className="space-y-2">
<label className="text-sm font-medium" htmlFor="sw-threshold">
Threshold
</label>
<input
id="sw-threshold"
type="number"
min={1}
max={config.owners.length}
value={config.threshold}
onChange={(e) =>
setConfig((c) => ({ ...c, threshold: Number(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"
/>
</div>
<div className="flex gap-3">
<button
onClick={() => setStep('type')}
className="flex-1 rounded-md border border-border px-4 py-2 text-sm font-medium hover:bg-accent"
>
Back
</button>
<button
onClick={() => setStep('advanced')}
className="flex-1 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
>
Next
</button>
</div>
</section>
)}
{/* Step 3: Advanced options */}
{step === 'advanced' && (
<section className="space-y-6">
<h2 className="text-sm font-medium uppercase tracking-wider text-muted-foreground">
Step 3: Advanced Options (Optional)
</h2>
<div className="space-y-2">
<label className="text-sm font-medium" htmlFor="factory">
Factory Address
</label>
<input
id="factory"
type="text"
value={config.factoryAddress}
onChange={(e) =>
setConfig((c) => ({ ...c, factoryAddress: e.target.value }))
}
placeholder="Leave empty for default"
className="w-full rounded-md border border-input bg-background px-3 py-2 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-ring"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium" htmlFor="salt">
Salt (CREATE2)
</label>
<input
id="salt"
type="text"
value={config.salt}
onChange={(e) =>
setConfig((c) => ({ ...c, salt: e.target.value }))
}
placeholder="Leave empty for random"
className="w-full rounded-md border border-input bg-background px-3 py-2 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-ring"
/>
</div>
<div className="flex gap-3">
<button
onClick={() => setStep('config')}
className="flex-1 rounded-md border border-border px-4 py-2 text-sm font-medium hover:bg-accent"
>
Back
</button>
<button
onClick={() => setStep('deploy')}
className="flex-1 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
>
Review & Deploy
</button>
</div>
</section>
)}
{/* Step 4: Deploy */}
{step === 'deploy' && !deployedAddress && (
<section className="space-y-6">
<h2 className="text-sm font-medium uppercase tracking-wider text-muted-foreground">
Step 4: Deploy
</h2>
<div className="rounded-lg border border-border bg-card p-6 space-y-3 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Type</span>
<span className="font-mono">
{config.type === 'safe' ? 'Gnosis Safe' : 'ERC-4337'}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Chain</span>
<span>
{config.chain} ({config.chainId})
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Threshold</span>
<span className="font-mono">
{config.threshold}-of-{config.owners.filter(Boolean).length}
</span>
</div>
<div>
<span className="text-muted-foreground">Owners</span>
<ul className="mt-1 space-y-1">
{config.owners.filter(Boolean).map((o, i) => (
<li key={i} className="truncate font-mono text-xs">
{o}
</li>
))}
</ul>
</div>
</div>
{error && (
<p className="text-sm text-destructive">{error}</p>
)}
<div className="flex gap-3">
<button
onClick={() => setStep('advanced')}
disabled={deploying}
className="flex-1 rounded-md border border-border px-4 py-2 text-sm font-medium hover:bg-accent disabled:opacity-50"
>
Back
</button>
<button
onClick={handleDeploy}
disabled={deploying}
className="flex-1 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
>
{deploying ? 'Deploying...' : 'Deploy'}
</button>
</div>
{deploying && (
<div className="flex items-center justify-center gap-3 py-4">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
<span className="text-sm text-muted-foreground">
Deploying contract...
</span>
</div>
)}
</section>
)}
{/* Success */}
{deployedAddress && (
<section className="rounded-lg border border-green-600/30 bg-green-600/10 p-6 text-center space-y-4">
<p className="text-lg font-medium text-green-500">
Smart Wallet Deployed
</p>
<p className="truncate font-mono text-sm text-green-500/80">
{deployedAddress}
</p>
<button
onClick={() =>
router.push(`/wallets/${params.id}/smart-wallets`)
}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
>
View Smart Wallets
</button>
</section>
)}
</div>
)
}