Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 29 additions & 28 deletions docs/general/system-flow.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# StartupChain System Flow

> **Last Updated:** February 23, 2026
> **Last Updated:** March 8, 2026
> **Status:** Early Production
> **Important:** Keep this diagram updated when making architectural changes.

Expand All @@ -14,6 +14,7 @@ StartupChain is an onchain company OS that allows founders to:
- **Hybrid registration:** Server handles ENS commit/register/Safe deploy → **User signs** final `recordCompany()` tx
- Manage from a unified dashboard
- **ENS management (proposal-first):** founders propose ENS trait/subdomain updates via Safe queue, UI reflects onchain confirmation
- **Safe proposal auth:** ENS proposals require the authenticated founder wallet to match the submitted signer and that signer must be a Safe owner
- **Session persistence:** Registration state saved in cookie for page refresh resilience

**Core Flow:** `ENS Check → Auth → Setup Wizard → Prepay to Treasury → (Auto) Commit → Wait 60s → Deploy Safe → Register ENS (to Safe) → **User Signs recordCompany()** → Dashboard`
Expand Down Expand Up @@ -293,18 +294,17 @@ StartupChain is an onchain company OS that allows founders to:
│ ┌────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ /dashboard/ens - ENS Management Page │ │
│ │ ┌──────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ • ENS profile traits (avatar/description/url) with proposal-first updates │ │ │
│ │ │ • Subdomain create/revoke proposals submitted to Safe queue │ │ │
│ │ │ • Identity tab handles ENS traits + subdomain profile/primary-name actions │ │ │
│ │ │ • Team tab handles founder batch subdomains + custom create/revoke actions │ │ │
│ │ │ • Pending proposal badges + periodic refresh until onchain confirmation │ │ │
│ │ │ • Renewal tab auto-quotes rentPrice and prefills payable renewal value │ │ │
│ │ │ • Links to Safe queue + explorer traces for registration/proposal txs │ │ │
│ │ └──────────────────────────────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────┘
```



---

## Smart Contracts (On-Chain)
Expand Down Expand Up @@ -369,6 +369,11 @@ StartupChain is an onchain company OS that allows founders to:
│ ├── getPendingTransactions(address) → queued txs needing signatures │
│ └── getTransactionHistory(address) → executed transactions │
│ │
│ ENS Proposal Rules: │
│ ├── Session wallet must match senderAddress │
│ ├── Sender must be listed in Safe owners │
│ └── Non-owner/delegate proposals are rejected before Safe proposal submission │
│ │
│ Rate limits: 5 requests/second (free tier) │
│ Networks: Mainnet + Sepolia supported │
│ File: src/lib/blockchain/safe-api.ts │
Expand Down Expand Up @@ -396,21 +401,21 @@ StartupChain is an onchain company OS that allows founders to:

### State Management


| Layer | Mechanism | Purpose |
| -------- | ---------------------------------------- | ----------------------------------------- |
| Server | Cookies (`pending-ens`, `privy-token`) | Session & registration state |
| Server | `getServerSession()` | Auth verification |
| Server | `TREASURY_ADDRESS` | Address for receiving user prepayments |
| Server | `SAFE_API_KEY` | Safe Transaction Service authentication |
| Client | `useDraftStore` | Setup wizard form state (chain-aware) |
| Client | `useWalletAuth` context | Auth state, chainId & methods |
| Client | `useCompanyRegistration` | Full registration flow (hybrid mode) |
| Client | `useSendTransaction` | User payment to treasury |
| Client | `useWriteContract` | User signs recordCompany() tx |
| Client | `resumeRegistrationAction()` | Session resume on page refresh |
| Client | React Query | Async data fetching (chainId in keys) |
| URL | `?chain=<chainId>` search param | Chain selection for server components |
| Layer | Mechanism | Purpose |
| ------ | -------------------------------------- | -------------------------------------------------------------------- |
| Server | Cookies (`pending-ens`, `privy-token`) | Session & registration state |
| Server | `getServerSession()` | Auth verification |
| Server | `TREASURY_ADDRESS` | Address for receiving user prepayments |
| Server | `SAFE_API_KEY` | Safe Transaction Service authentication |
| Client | `useDraftStore` | Setup wizard form state (chain-aware) |
| Client | `useWalletAuth` context | Auth state, chainId & methods |
| Client | `useSafeWallet` | Match the authenticated founder wallet before signing Safe proposals |
| Client | `useCompanyRegistration` | Full registration flow (hybrid mode) |
| Client | `useSendTransaction` | User payment to treasury |
| Client | `useWriteContract` | User signs recordCompany() tx |
| Client | `resumeRegistrationAction()` | Session resume on page refresh |
| Client | React Query | Async data fetching (chainId in keys) |
| URL | `?chain=<chainId>` search param | Chain selection for server components |

**Registration States (PendingStatus):**

Expand Down Expand Up @@ -449,9 +454,8 @@ The app supports multiple chains (Sepolia, Mainnet) with chain-aware data fetchi

**Key Components:**


| Component | Chain Handling |
| -------------------------------------- | ------------------------------------------------------------- |
| ------------------------------------ | ----------------------------------------------------------- |
| `NetworkSwitcher` | Switches wallet chain + pushes`?chain=<id>` URL param |
| `getPublicClient(chainId)` | Returns cached Viem client for specific chain |
| `getCompanyByAddress(addr, chainId)` | Queries correct chain's StartupChain contract |
Expand All @@ -468,9 +472,8 @@ The app supports multiple chains (Sepolia, Mainnet) with chain-aware data fetchi

## Key Technologies


| Category | Technologies |
| ---------------- | ---------------------------------------------------------------------------------- |
| -------------- | -------------------------------------------------------------------------------- |
| **Frontend** | Next.js 16 (App Router), React 19 (Server Components), TailwindCSS v4, shadcn/ui |
| **Auth** | Privy (wallet auth), JWT tokens, HTTP-only cookies |
| **Blockchain** | Viem (client), Wagmi (hooks), @ensdomains/ensjs, Custom Solidity contracts |
Expand All @@ -481,9 +484,8 @@ The app supports multiple chains (Sepolia, Mainnet) with chain-aware data fetchi

## Key Files Reference


| File | Purpose |
| ------------------------------------------------------------- | ------------------------------------------------------- |
| ----------------------------------------------------------- | ----------------------------------------------------- |
| `src/app/(public)/page.tsx` | Landing page with ENS checker |
| `src/components/ens-name-checker/EnsNameChecker.tsx` | ENS availability checking UI |
| `src/components/ens-name-checker/useEnsCheck.ts` | ENS check hook (chain-aware query keys) |
Expand Down Expand Up @@ -514,9 +516,8 @@ The app supports multiple chains (Sepolia, Mainnet) with chain-aware data fetchi

**Hybrid model: User sends ETH to treasury for ENS/Safe costs, server executes ENS + Safe txs, then user signs final recordCompany() tx.**


| Cost Component | Paid By | Recipient |
| ---------------------------------- | ------------------------ | ------------------------------- |
| -------------------------------- | ---------------------- | ----------------------------- |
| ENS Registration (1 year) | Server (from treasury) | ENS Protocol |
| Safe Deployment Gas | Server (from treasury) | Network |
| Service Fee (25% of ENS cost) | Server (from treasury) | StartupChain (`feeRecipient`) |
Expand Down
2 changes: 1 addition & 1 deletion src/app/(app)/dashboard/components/company-token.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ExternalLink, MessageCircle, Wallet2 } from 'lucide-react'
import { ExternalLink, Wallet2 } from 'lucide-react'

import { Button } from '@/components/ui/button'
import { shortenAddress } from '@/lib/utils'
Expand Down
19 changes: 8 additions & 11 deletions src/app/(app)/dashboard/components/dashboard-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useState } from 'react'

import { appNavItems, footerItems } from '@/app/(app)/dashboard/config/navigation'
import {
appNavItems,
footerItems,
} from '@/app/(app)/dashboard/config/navigation'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
Expand All @@ -25,7 +28,7 @@ type DashboardHeaderProps = {
}

export function DashboardHeader({ title }: DashboardHeaderProps) {
const { primaryAddress, disconnect, user } = useWalletAuth()
const { primaryAddress, disconnect } = useWalletAuth()
const router = useRouter()
const [isLoggingOut, setIsLoggingOut] = useState(false)

Expand All @@ -42,11 +45,6 @@ export function DashboardHeader({ title }: DashboardHeaderProps) {
}
}

const avatarFallback =
user?.email?.address?.[0]?.toUpperCase() ??
user?.id?.[0]?.toUpperCase() ??
'U'

return (
<div className="border-border bg-background/90 sticky top-0 z-10 border-b px-4 py-3 backdrop-blur md:px-6">
<div className="flex flex-col gap-3">
Expand All @@ -61,7 +59,7 @@ export function DashboardHeader({ title }: DashboardHeaderProps) {
priority
/>
<div className="leading-tight">
<p className="text-foreground text-lg font-semibold transition-colors duration-200 group-hover:text-primary">
<p className="text-foreground group-hover:text-primary text-lg font-semibold transition-colors duration-200">
StartUpChain
</p>
<p className="text-muted-foreground text-xs">Onchain OS</p>
Expand All @@ -86,7 +84,7 @@ export function DashboardHeader({ title }: DashboardHeaderProps) {
<DropdownMenuItem
key={item.url}
asChild
className="focus:bg-primary/10 focus:text-primary focus:translate-x-1 cursor-pointer transition-all duration-200"
className="focus:bg-primary/10 focus:text-primary cursor-pointer transition-all duration-200 focus:translate-x-1"
>
<Link href={item.url}>
<item.icon className="mr-2 h-4 w-4" />
Expand All @@ -99,7 +97,7 @@ export function DashboardHeader({ title }: DashboardHeaderProps) {
<DropdownMenuItem
key={item.url}
asChild
className="focus:bg-primary/10 focus:text-primary focus:translate-x-1 cursor-pointer transition-all duration-200"
className="focus:bg-primary/10 focus:text-primary cursor-pointer transition-all duration-200 focus:translate-x-1"
>
<Link href={item.url}>
<item.icon className="mr-2 h-4 w-4" />
Expand Down Expand Up @@ -163,7 +161,6 @@ export function DashboardHeader({ title }: DashboardHeaderProps) {
</Button>
</div>
</div>

</div>
</div>
)
Expand Down
1 change: 0 additions & 1 deletion src/app/(app)/dashboard/config/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
Coins,
LayoutDashboard,
LineChart,
LogOut,
Settings,
ShieldCheck,
WalletCards,
Expand Down
100 changes: 100 additions & 0 deletions src/app/(app)/dashboard/ens/actions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'

import { getEnsRenewalQuoteAction } from './actions'

const { mockReadContract, mockFetch } = vi.hoisted(() => ({
mockReadContract: vi.fn(),
mockFetch: vi.fn(),
}))

vi.mock('@/lib/blockchain/startupchain-client', () => ({
getPublicClient: () => ({
readContract: (...args: unknown[]) => mockReadContract(...args),
}),
}))

vi.stubGlobal('fetch', mockFetch)

describe('getEnsRenewalQuoteAction', () => {
beforeEach(() => {
mockReadContract.mockReset()
mockFetch.mockReset()
})

it('returns quote with USD estimate when Coinbase succeeds', async () => {
mockReadContract.mockResolvedValue({
base: 10000000000000000n,
premium: 2000000000000000n,
})
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ data: { amount: '2500.00' } }),
})

const result = await getEnsRenewalQuoteAction({
ensName: 'acme.eth',
durationSeconds: 31536000n,
chainId: 11155111,
})

expect(result).toMatchObject({
ok: true,
baseWei: '10000000000000000',
premiumWei: '2000000000000000',
totalWei: '12000000000000000',
totalEth: '0.012',
estimatedTotalUsd: '30.00',
usdEstimateSource: 'coinbase-spot',
})
})

it('returns ok: true with null USD when Coinbase fails', async () => {
mockReadContract.mockResolvedValue({
base: 10000000000000000n,
premium: 2000000000000000n,
})
mockFetch.mockRejectedValue(new Error('Network error'))

const result = await getEnsRenewalQuoteAction({
ensName: 'acme.eth',
durationSeconds: 31536000n,
chainId: 11155111,
})

expect(result).toMatchObject({
ok: true,
totalWei: '12000000000000000',
totalEth: '0.012',
estimatedTotalUsd: null,
usdEstimateSource: null,
})
})

it('returns an error for unsupported chains', async () => {
const result = await getEnsRenewalQuoteAction({
ensName: 'acme.eth',
durationSeconds: 31536000n,
chainId: 10,
})

expect(result).toEqual({
ok: false,
error: 'Unsupported chain for ENS renewal quote.',
})
expect(mockReadContract).not.toHaveBeenCalled()
})

it('returns an error for invalid durations', async () => {
const result = await getEnsRenewalQuoteAction({
ensName: 'acme.eth',
durationSeconds: 0n,
chainId: 11155111,
})

expect(result).toEqual({
ok: false,
error: 'Renewal duration must be greater than zero.',
})
expect(mockReadContract).not.toHaveBeenCalled()
})
})
Loading