Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d7cd87e
feat(webapp): enter affiliate fees as percent, not bps
Wieedze Jun 19, 2026
b694bf7
feat(webapp): restructure Home hero (text+CTA left, live stats right)
Wieedze Jun 19, 2026
e648a05
style(webapp): lengthen home beam top-to-callflow
Wieedze Jun 19, 2026
1fcd756
style(webapp): center the narrow page containers
Wieedze Jun 19, 2026
b6c7b53
style(webapp): consolidate affiliate view into two glassy cards
Wieedze Jun 19, 2026
802b68b
feat(webapp): tabs on My affiliate (Analytics / Integration / Config)
Wieedze Jun 19, 2026
c96c8a9
style(webapp): split analytics into two title-less blocks
Wieedze Jun 19, 2026
6d04673
style(webapp): drop the top-edge hairline on card first row
Wieedze Jun 19, 2026
829f1fb
style(webapp): flush first-row cells to card top (drop pt)
Wieedze Jun 19, 2026
3bf3f92
style(webapp): drop title + status badge from config card
Wieedze Jun 19, 2026
80378b3
style(webapp): drop 'How it works' label, keep call-flow position
Wieedze Jun 19, 2026
53bab82
feat(webapp): docs quickstart + copy-paste agent integration guide
Wieedze Jun 19, 2026
cccbe1a
style(webapp): move agent-guide button into Quickstart, drop docs header
Wieedze Jun 19, 2026
4ca0444
fix(webapp): render beam on first paint (re-measure on mount)
Wieedze Jun 19, 2026
2a76f73
fix(webapp): keep beam in-view so the fade-in never freezes
Wieedze Jun 19, 2026
77e9e71
chore: ignore nested contracts clone + refresh lockfile
Wieedze Jun 19, 2026
f68f84f
style(webapp): drop orange glow on affiliate cards
Wieedze Jun 19, 2026
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,6 @@ forge-cache/

# Auto-synced ABIs
packages/sdk/src/abis/*.json

#intuition contract
intuition-contracts-v2
1,558 changes: 45 additions & 1,513 deletions bun.lock

Large diffs are not rendered by default.

138 changes: 92 additions & 46 deletions packages/webapp/src/components/AffiliateView.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,109 @@
import type { ReactNode } from 'react'

import type { AffiliateConfig, AffiliateStats } from '../contracts'
import Address from './Address'
import { Stat } from './Stat'
import { Metric } from './Metric'
import { formatBps, formatDateParts, formatTrust } from '../lib/format'

/** Read-only render of an affiliate's registry row. */
export function AffiliateConfigCard({ config }: { config: AffiliateConfig }) {
const reg = formatDateParts(Number(config.registeredAt))
/** Glassy card matching the home stats card, with a kicker title + cell grid. */
function Card({
title,
badge,
cols,
children,
}: {
title?: string
badge?: ReactNode
cols: 2 | 3
children: ReactNode
}) {
const hasHeader = Boolean(title || badge)
// Drop the hairline on the first row so it never sits at the card's top edge;
// inner rows keep their top border as a separator.
const firstRowFlat =
cols === 3
? '[&>*:nth-child(-n+3)]:border-t-0 [&>*:nth-child(-n+3)]:pt-0'
: '[&>*:nth-child(-n+2)]:border-t-0 [&>*:nth-child(-n+2)]:pt-0'
return (
<section className="space-y-4">
<div className="flex items-center gap-3">
<h2 className="font-semibold text-ink">Configuration</h2>
{config.paused ? (
<span className="text-[10px] font-mono uppercase tracking-wider text-rose-400 border border-rose-400/40 rounded px-1.5 py-0.5">
Paused
</span>
) : (
<span className="text-[10px] font-mono uppercase tracking-wider text-brand border border-brand/40 rounded px-1.5 py-0.5">
Active
</span>
)}
<section className="rounded-2xl border border-line bg-surface/70 p-6 backdrop-blur-md">
{hasHeader && (
<div className="flex items-center gap-3">
{title && (
<h2 className="text-[11px] font-medium uppercase tracking-wider text-subtle">
{title}
</h2>
)}
{badge}
</div>
)}
<div
className={`grid gap-x-6 gap-y-5 ${firstRowFlat} ${hasHeader ? 'mt-5' : ''} ${
cols === 3 ? 'sm:grid-cols-3' : 'sm:grid-cols-2'
}`}
>
{children}
</div>
</section>
)
}

<div className="grid gap-3 sm:grid-cols-2">
<Stat label="Fee recipient" value={config.feeRecipient} mono />
<Stat label="Registered" value={`${reg.date} ${reg.time}`} />
<Stat
label="Deposit fee"
value={`${formatBps(config.fees.depositBps)} + ${formatTrust(config.fees.depositFixedFee)} TRUST`}
/>
<Stat
label="Creation fee"
value={`${formatBps(config.fees.creationBps)} + ${formatTrust(config.fees.creationFixedFee)} TRUST`}
/>
function Cell({
label,
children,
emphasize,
}: {
label: string
children: ReactNode
emphasize?: boolean
}) {
return (
<div className="space-y-1 border-t border-line pt-3">
<div className="text-[11px] uppercase tracking-wider text-subtle">{label}</div>
<div className={`text-lg font-semibold ${emphasize ? 'text-brand' : 'text-ink'}`}>
{children}
</div>
</div>
)
}

<div className="text-xs text-subtle inline-flex items-center gap-2">
Recipient: <Address value={config.feeRecipient} variant="short" />
</div>
</section>
/** Read-only render of an affiliate's registry row. */
export function AffiliateConfigCard({ config }: { config: AffiliateConfig }) {
const reg = formatDateParts(Number(config.registeredAt))

return (
<Card cols={2}>
<Cell label="Fee recipient">
<Address value={config.feeRecipient} variant="short" />
</Cell>
<Cell label="Registered">{`${reg.date} ${reg.time}`}</Cell>
<Cell label="Deposit fee">
{`${formatBps(config.fees.depositBps)} + ${formatTrust(config.fees.depositFixedFee)} TRUST`}
</Cell>
<Cell label="Creation fee">
{`${formatBps(config.fees.creationBps)} + ${formatTrust(config.fees.creationFixedFee)} TRUST`}
</Cell>
</Card>
)
}

/** Read-only render of an affiliate's aggregate analytics. */
export function AffiliateStatsCard({ stats }: { stats: AffiliateStats }) {
return (
<section className="space-y-4">
<h2 className="font-semibold text-ink">Analytics</h2>
<div className="grid gap-3 sm:grid-cols-3">
<Metric label="Transactions" value={stats.txCount.toString()} />
<Metric label="Unique users" value={stats.uniqueUsers.toString()} />
<Metric label="Total fees" value={`${formatTrust(stats.totalFees)} TRUST`} emphasize />
<Metric label="Gross routed" value={`${formatTrust(stats.totalGrossAssets)} TRUST`} />
<Metric label="Forwarded" value={`${formatTrust(stats.totalForwardedAssets)} TRUST`} />
<Metric label="Deposits" value={stats.depositCount.toString()} />
<Metric label="Atom/triple creations" value={stats.creationCount.toString()} />
<Metric label="Deposit fees" value={`${formatTrust(stats.depositFees)} TRUST`} />
<Metric label="Creation fees" value={`${formatTrust(stats.creationFees)} TRUST`} />
</div>
</section>
<div className="space-y-4">
<Card cols={3}>
<Cell label="Transactions">{stats.txCount.toString()}</Cell>
<Cell label="Unique users">{stats.uniqueUsers.toString()}</Cell>
<Cell label="Total fees" emphasize>
{`${formatTrust(stats.totalFees)} TRUST`}
</Cell>
</Card>
<Card cols={3}>
<Cell label="Gross routed">{`${formatTrust(stats.totalGrossAssets)} TRUST`}</Cell>
<Cell label="Forwarded">{`${formatTrust(stats.totalForwardedAssets)} TRUST`}</Cell>
<Cell label="Deposits">{stats.depositCount.toString()}</Cell>
<Cell label="Atom/triple creations">{stats.creationCount.toString()}</Cell>
<Cell label="Deposit fees">{`${formatTrust(stats.depositFees)} TRUST`}</Cell>
<Cell label="Creation fees">{`${formatTrust(stats.creationFees)} TRUST`}</Cell>
</Card>
</div>
)
}
55 changes: 39 additions & 16 deletions packages/webapp/src/components/FeeConfigFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,39 @@ import type { FeeConfig } from '../contracts'
import { formatBps, formatTrust } from '../lib/format'

export type FeeFields = {
depositBps: string
creationBps: string
/** Percentage fees entered as a human percent (e.g. "1.5" = 1.5%). */
depositPct: string
creationPct: string
/** Fixed fees entered as a decimal TRUST amount (converted to wei). */
depositFixedFee: string
creationFixedFee: string
}

/**
* Convert a human percentage string to on-chain bps (basis points, base 10000).
* "1" → 100, "2.5" → 250, "0.05" → 5. Max 2 decimals (1 bps = 0.01%).
*/
function pctToBps(pct: string): bigint {
const t = (pct || '0').trim()
if (!/^\d+(\.\d{1,2})?$/.test(t)) {
throw new Error(`Invalid percentage "${pct}" — use up to 2 decimals (e.g. 1.5)`)
}
return BigInt(Math.round(parseFloat(t) * 100))
}

/** Convert on-chain bps to a clean percentage string. 250 → "2.5", 5 → "0.05". */
function bpsToPct(bps: bigint): string {
const whole = bps / 100n
const frac = bps % 100n
if (frac === 0n) return whole.toString()
const fracStr = frac.toString().padStart(2, '0').replace(/0$/, '')
return `${whole}.${fracStr}`
}

/** Parse the string form fields into an on-chain `FeeConfig` (bigints). */
export function parseFeeFields(f: FeeFields): FeeConfig {
const depositBps = BigInt(f.depositBps || '0')
const creationBps = BigInt(f.creationBps || '0')
const depositBps = pctToBps(f.depositPct)
const creationBps = pctToBps(f.creationPct)
const depositFixedFee = parseEther((f.depositFixedFee || '0').trim())
const creationFixedFee = parseEther((f.creationFixedFee || '0').trim())
return { depositBps, creationBps, depositFixedFee, creationFixedFee }
Expand All @@ -22,8 +44,8 @@ export function parseFeeFields(f: FeeFields): FeeConfig {
/** Build editable string fields from an on-chain `FeeConfig`. */
export function feeFieldsFrom(fees: FeeConfig): FeeFields {
return {
depositBps: fees.depositBps.toString(),
creationBps: fees.creationBps.toString(),
depositPct: bpsToPct(fees.depositBps),
creationPct: bpsToPct(fees.creationBps),
depositFixedFee: formatTrust(fees.depositFixedFee),
creationFixedFee: formatTrust(fees.creationFixedFee),
}
Expand All @@ -38,15 +60,16 @@ interface Props {

/**
* Shared fee-schedule editor used by registration and fee-update flows.
* bps are integers (base 10000); fixed fees are entered in TRUST.
* Percentages are entered as human percents and converted to bps on submit;
* fixed fees are entered in TRUST.
*/
export function FeeConfigFields({ fields, onChange, maxBps, maxFixedFee }: Props) {
const set = (key: keyof FeeFields) => (e: React.ChangeEvent<HTMLInputElement>) =>
onChange({ ...fields, [key]: e.target.value })

const capHint =
maxBps !== undefined
? `Caps: max ${maxBps.toString()} bps (${formatBps(maxBps)})${
? `Caps: max ${formatBps(maxBps)}${
maxFixedFee !== undefined ? `, max ${formatTrust(maxFixedFee)} TRUST fixed` : ''
}`
: null
Expand All @@ -56,17 +79,17 @@ export function FeeConfigFields({ fields, onChange, maxBps, maxFixedFee }: Props
<div className="text-xs text-subtle">Fee schedule{capHint ? ` — ${capHint}` : ''}</div>
<div className="grid gap-4 sm:grid-cols-2">
<Field
label="Deposit bps"
hint="base 10000"
value={fields.depositBps}
onChange={set('depositBps')}
label="Deposit fee (%)"
hint="e.g. 1.5 for 1.5%"
value={fields.depositPct}
onChange={set('depositPct')}
mono
/>
<Field
label="Creation bps"
hint="base 10000"
value={fields.creationBps}
onChange={set('creationBps')}
label="Creation fee (%)"
hint="e.g. 1.5 for 1.5%"
value={fields.creationPct}
onChange={set('creationPct')}
mono
/>
<Field
Expand Down
11 changes: 10 additions & 1 deletion packages/webapp/src/components/LaserFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -427,14 +427,21 @@ export const LaserFlow = ({
}

setSizeNow()
// First-paint robustness: re-measure over the first frames so the beam
// renders as soon as the container has its real height (no long blank wait).
const sizeRetryRaf = requestAnimationFrame(setSizeNow)
const sizeRetryTimers = [60, 180, 400, 800].map((ms) => setTimeout(setSizeNow, ms))
const ro = new ResizeObserver(scheduleResize)
ro.observe(mount)

const io = new IntersectionObserver(
(entries) => {
inViewRef.current = entries[0]?.isIntersecting ?? true
},
{ root: null, threshold: 0 },
// Generous margin: the beam sits at the top of the page, so keep it
// "in view" even before layout settles — otherwise a 0-height first
// measurement reports not-intersecting and freezes the fade-in.
{ root: null, threshold: 0, rootMargin: '600px' },
)
io.observe(mount)

Expand Down Expand Up @@ -551,6 +558,8 @@ export const LaserFlow = ({

return () => {
cancelAnimationFrame(raf)
cancelAnimationFrame(sizeRetryRaf)
sizeRetryTimers.forEach(clearTimeout)
ro.disconnect()
io.disconnect()
document.removeEventListener('visibilitychange', onVis)
Expand Down
6 changes: 3 additions & 3 deletions packages/webapp/src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,14 @@ export default function Layout() {
<main className="relative flex-1">
<div
aria-hidden
className={`hidden dark:block pointer-events-none absolute inset-x-0 top-0 h-[820px] z-0 transition-opacity ease-out ${
className={`hidden dark:block pointer-events-none absolute inset-x-0 top-0 h-[1400px] z-0 transition-opacity ease-out ${
isHome ? 'opacity-100 duration-1000' : 'opacity-0 duration-100'
}`}
style={{
maskImage:
'linear-gradient(to bottom, transparent 0px, black 120px)',
'linear-gradient(to bottom, transparent 0px, black 32px)',
WebkitMaskImage:
'linear-gradient(to bottom, transparent 0px, black 120px)',
'linear-gradient(to bottom, transparent 0px, black 32px)',
}}
>
<div className="mx-auto max-w-6xl h-full px-6">
Expand Down
60 changes: 60 additions & 0 deletions packages/webapp/src/hooks/useProtocolStats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useMemo } from 'react'
import type { Abi } from 'viem'
import { useReadContracts } from 'wagmi'

import { FeeProxyABI, type AffiliateStats } from '../contracts'

import { useAffiliates } from './useAffiliates'
import { useFeeProxyAddress } from './useFeeProxyAddress'

/**
* Network-wide aggregate stats for the Home hero card. Enumerates affiliates
* from registration events, then batch-reads each `affiliateStats` and sums
* the headline figures. No indexer needed; fine for the current scale.
*/
export function useProtocolStats() {
const { feeProxy, configured } = useFeeProxyAddress()
const { affiliates, isLoading: listLoading } = useAffiliates()

const contracts = useMemo(
() =>
affiliates.map((a) => ({
address: feeProxy,
abi: FeeProxyABI as Abi,
functionName: 'affiliateStats',
args: [a.affiliate],
})),
[affiliates, feeProxy],
)

const { data, isLoading: statsLoading } = useReadContracts({
contracts: contracts as any,
query: { enabled: configured && affiliates.length > 0 },
})

const agg = useMemo(() => {
let totalForwarded = 0n
let totalFees = 0n
let totalGross = 0n
let totalTx = 0n
if (data) {
for (const r of data) {
if (r.status === 'success' && r.result) {
const s = r.result as unknown as AffiliateStats
totalForwarded += s.totalForwardedAssets
totalFees += s.totalFees
totalGross += s.totalGrossAssets
totalTx += s.txCount
}
}
}
return { totalForwarded, totalFees, totalGross, totalTx }
}, [data])

return {
affiliateCount: affiliates.length,
...agg,
configured,
isLoading: listLoading || statsLoading,
}
}
4 changes: 2 additions & 2 deletions packages/webapp/src/pages/AffiliateDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default function AffiliateDetailPage() {

if (!valid) {
return (
<div className="max-w-2xl space-y-4">
<div className="mx-auto max-w-2xl space-y-4">
<p className="text-sm text-rose-400 font-mono">Invalid affiliate address.</p>
<Link to="/affiliates" className="text-sm text-brand underline">
← Back to affiliates
Expand All @@ -36,7 +36,7 @@ export default function AffiliateDetailPage() {
}

return (
<div className="max-w-3xl space-y-8">
<div className="mx-auto max-w-3xl space-y-8">
<header className="space-y-2">
<Link to="/affiliates" className="text-xs text-muted hover:text-ink transition-colors">
← Affiliates
Expand Down
Loading
Loading