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
7 changes: 7 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,13 @@ as runtime requires, not webpack-bundled) and pins `outputFileTracingRoot` to th
workspace-dep tracing is correct. On Vercel: Root Directory = `dashboard`, Build Command =
`pnpm run build`.

The workspace packages themselves are **not** externalised — they are bundled into the serverless
functions, and therefore **minified**, which renames classes and functions. Nothing that is
persisted or published may be derived from a runtime identifier (`constructor.name`, `fn.name`):
those values are correct under `node --test` and `next dev` and wrong in the only environment that
writes the data. `ProtocolMetadata.adapterRef` is a hardcoded literal for exactly this reason — see
[`CONTRIBUTING.md`](CONTRIBUTING.md#adapterref-must-be-a-hardcoded-string-literal).

**Tests:** `pnpm test` at the root, fanning out to whichever packages define one, and **run by CI on
every PR**. There is **no test framework dependency** — tests are `*.test.ts` files run by Node's
built-in test runner (`node --test`) against native TypeScript stripping, which is why CI and
Expand Down
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ These override any default behavior and are enforced in code and review:
so a heterogeneous adapter list shares one typed run loop. `core/src/adapter.ts` carries
`ADAPTER_INTERFACE_VERSION` — bump it for future breaking interface changes rather than rewriting
every adapter at once.
- **Nothing persisted or published may come from a runtime identifier.** No `constructor.name`,
`fn.name`, or similar for a value that reaches the database or an API response — use a string
literal. The workspace packages are bundled and minified into the dashboard's serverless
functions, so identifiers are renamed there and _only_ there: such a value is correct in every
test and in local dev, and wrong in production. Full rationale in [`CONTRIBUTING.md`](CONTRIBUTING.md).
- **Tests are `node --test`, zero dependencies.** `*.test.ts` files run on Node's built-in runner
via native type stripping (`pnpm test`); test files import with an explicit `.ts` extension, app
code doesn't. Test pure logic whose important cases live data can't reach — not for coverage.
Expand Down
40 changes: 39 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ it stays internal to your adapter.

```ts
export interface Adapter<TRawData = unknown> {
readonly metadata: ProtocolMetadata; // { id: slug, name, chain: 'stellar' }
// { id: slug, name, chain: 'stellar', adapterRef: 'YourAdapter' }
readonly metadata: ProtocolMetadata;

fetchRawData(): Promise<TRawData>; // pull raw on-chain state (RPC + Horizon)

Expand All @@ -54,6 +55,43 @@ export interface Adapter<TRawData = unknown> {
Three separate methods (not one `run()`) so the indexer can inspect intermediate output and so
`score()` can be unit-tested against fixed factor inputs without touching RPC.

### `adapterRef` must be a hardcoded string literal

`metadata.adapterRef` names the class that produced the score (`'BlendAdapter'`). It is stored in
`protocols.adapter` and published on `GET /api/v1/protocol/:id`, where it is the provenance label a
reader follows to find your adapter in this repo.

Write it out by hand:

```ts
readonly metadata: ProtocolMetadata = {
id: 'yourprotocol',
name: 'YourProtocol',
chain: 'stellar',
adapterRef: 'YourProtocolAdapter', // literal — never this.constructor.name
};
```

**Never derive it from the class name** — not `this.constructor.name`, not
`YourAdapter.name`, not anything else read off a runtime identifier. It looks like the obvious
DRY move and it is silently wrong in production:

The indexer is not a standalone deployed process. It runs inside the dashboard's serverless cron
route (`dashboard/app/api/cron/run-indexer`), which imports `@stenion/indexer` and, through it,
your adapter class. Next bundles the workspace packages into that function and **minifies them**,
which renames classes — `class BlendAdapter` becomes `class w`. `constructor.name` then returns
`'w'`, and that is what gets written to the database.

Nothing catches this before it ships. It is correct under `node --test`, correct under `next dev`,
and correct in every local run, because none of those minify. It is wrong only in the one
environment that actually writes the published data. This is not hypothetical: it is exactly how
every row in `protocols` came to have `adapter = 'w'` for both shipped protocols until it was
fixed. A string literal is a value the bundler has no license to rewrite; a class name is not.

The same reasoning applies to anything else an adapter persists or publishes. If a value ends up in
the database or an API response, it must come from a literal or from on-chain data — never from a
JavaScript identifier.

## The `*Safety` taxonomy — populate all five

Every adapter must populate the same fixed five factors from `RiskFactorType`
Expand Down
2 changes: 2 additions & 0 deletions adapters/blend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,8 @@ export class BlendAdapter implements Adapter<BlendRawData> {
id: 'blend',
name: 'Blend',
chain: 'stellar',
// Literal, not this.constructor.name — see ProtocolMetadata.adapterRef.
adapterRef: 'BlendAdapter',
};

private readonly rpcUrl: string;
Expand Down
2 changes: 2 additions & 0 deletions adapters/kinetic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,8 @@ export class KineticAdapter implements Adapter<KineticRawData> {
id: 'kinetic',
name: 'Kinetic',
chain: 'stellar',
// Literal, not this.constructor.name — see ProtocolMetadata.adapterRef.
adapterRef: 'KineticAdapter',
};

private readonly rpcUrl: string;
Expand Down
15 changes: 15 additions & 0 deletions core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ export interface ProtocolMetadata {
id: string;
name: string;
chain: Chain;
/**
* Which adapter produced this protocol's scores, e.g. "BlendAdapter".
* Persisted to `protocols.adapter` and published on GET /api/v1/protocol/:id
* as the provenance label a reader uses to find the adapter in the repo.
*
* MUST be a string literal. Never `this.constructor.name` or anything else
* derived from a runtime identifier: the workspace packages are bundled and
* minified into the dashboard's serverless functions, which renames the
* classes. Deriving it is how every row in `protocols` came to read `w`
* instead of `BlendAdapter`/`KineticAdapter` — correct in every test and in
* local dev, wrong in the only environment that actually writes the data.
* It sits here with id/name/chain because it is the same kind of value:
* a fixed string the build cannot rewrite.
*/
adapterRef: string;
}

/**
Expand Down
46 changes: 20 additions & 26 deletions dashboard/app/(home)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
Coins,
Gauge,
KeyRound,
Radio,
ShieldOff,
Timer,
Waves,
Expand All @@ -36,27 +35,22 @@ export default async function HomePage() {
<>
{/* ---------- Hero ---------- */}
<section className="relative overflow-hidden">
{/* The dotted backdrop stays (white-alpha texture, no hue). The teal
radial glow that used to sit here is gone: an ambient wash of the
accent is the accent behaving as decoration, which is exactly what
this palette is built to avoid. */}
<div className="grid-backdrop pointer-events-none absolute inset-0" />
<div
className="pointer-events-none absolute -top-40 left-1/2 h-[420px] w-[820px] -translate-x-1/2 rounded-full opacity-25 blur-3xl"
style={{
background: 'radial-gradient(closest-side, var(--color-accent), transparent 70%)',
}}
/>
<div className="relative mx-auto max-w-6xl px-5 pb-20 pt-20 sm:pt-28">
<Reveal>
<span className="inline-flex items-center gap-2 rounded-full border border-line bg-surface/60 px-3 py-1 text-xs text-muted">
<Radio className="h-3.5 w-3.5 text-accent" />
<span className="text-xs font-medium uppercase tracking-[0.18em] text-muted">
Live risk intelligence for Stellar / Soroban DeFi
</span>
</Reveal>

<Reveal delay={0.06}>
<h1 className="mt-6 max-w-3xl font-display text-4xl font-semibold leading-[1.08] tracking-tight text-ink sm:text-6xl">
Audits are a snapshot.{' '}
<span className="bg-gradient-to-r from-accent to-accent-2 bg-clip-text text-transparent">
Risk moves every block.
</span>
Audits are a snapshot. Risk moves <span className="text-accent-ink">every block</span>
.
</h1>
</Reveal>

Expand All @@ -72,14 +66,14 @@ export default async function HomePage() {
<div className="mt-9 flex flex-wrap items-center gap-3">
<Link
href="/registry"
className="group inline-flex items-center gap-2 rounded-lg bg-gradient-to-r from-accent to-accent-2 px-5 py-2.5 text-sm font-semibold text-bg transition-transform hover:-translate-y-0.5"
className="group inline-flex items-center gap-2 rounded-lg bg-accent px-5 py-2.5 text-sm font-semibold text-bg transition-transform hover:-translate-y-0.5"
>
Explore the registry
<ArrowRight className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
</Link>
<Link
href="/methodology"
className="inline-flex items-center gap-2 rounded-lg border border-line bg-surface/60 px-5 py-2.5 text-sm font-medium text-ink transition-colors hover:border-accent/40"
className="inline-flex items-center gap-2 rounded-lg border border-line-strong px-5 py-2.5 text-sm font-medium text-ink transition-colors hover:border-accent"
>
Read the methodology
</Link>
Expand All @@ -102,7 +96,7 @@ export default async function HomePage() {
</div>
<Link
href="/registry"
className="hidden shrink-0 items-center gap-1 text-sm text-accent hover:text-accent-2 sm:inline-flex"
className="hidden shrink-0 items-center gap-1 text-sm text-accent-ink transition-colors hover:text-ink sm:inline-flex"
>
All protocols <ArrowRight className="h-4 w-4" />
</Link>
Expand All @@ -112,7 +106,7 @@ export default async function HomePage() {
{scored.length === 0 ? (
<div className="mt-6 rounded-xl border border-line bg-surface p-6 text-sm text-muted">
Live scores are warming up — the indexer hasn&apos;t published a run yet.{' '}
<Link href="/registry" className="text-accent hover:underline">
<Link href="/registry" className="text-accent-ink hover:underline">
Open the registry
</Link>{' '}
to check again.
Expand All @@ -123,7 +117,7 @@ export default async function HomePage() {
<RevealItem key={p.id}>
<Link
href={`/protocol/${p.id}`}
className="group flex items-center gap-5 rounded-xl border border-line surface-lit p-5 transition-all hover:-translate-y-0.5 hover:border-accent/40"
className="group flex items-center gap-5 rounded-xl border border-line surface-lit p-5 transition-all hover:-translate-y-0.5 hover:border-accent"
>
<ScoreRing score={p.safetyScore} size={104} stroke={8} label={null} />
<div className="min-w-0">
Expand Down Expand Up @@ -180,8 +174,8 @@ export default async function HomePage() {
].map((c) => (
<RevealItem key={c.title}>
<div className="h-full rounded-xl border border-line surface-lit p-6">
<div className="grid h-10 w-10 place-items-center rounded-lg bg-accent/10 ring-1 ring-inset ring-accent/20">
<c.icon className="h-5 w-5 text-accent" strokeWidth={2} />
<div className="grid h-10 w-10 place-items-center rounded-lg bg-surface-2 ring-1 ring-inset ring-line">
<c.icon className="h-5 w-5 text-muted" strokeWidth={2} />
</div>
<h3 className="mt-4 font-medium text-ink">{c.title}</h3>
<p className="mt-2 text-sm leading-relaxed text-muted">{c.body}</p>
Expand All @@ -208,7 +202,7 @@ export default async function HomePage() {
</div>
<Link
href="/methodology"
className="inline-flex items-center gap-1 text-sm text-accent hover:text-accent-2"
className="inline-flex items-center gap-1 text-sm text-accent-ink transition-colors hover:text-ink"
>
Full methodology <ArrowRight className="h-4 w-4" />
</Link>
Expand All @@ -225,7 +219,7 @@ export default async function HomePage() {
].map((f) => (
<RevealItem key={f.name}>
<div className="rounded-lg border border-line-soft bg-surface p-4">
<f.icon className="h-5 w-5 text-accent-2" strokeWidth={2} />
<f.icon className="h-5 w-5 text-muted" strokeWidth={2} />
<div className="mt-3 text-sm font-medium text-ink">{f.name}</div>
<div className="mt-0.5 text-xs text-faint">{f.hint}</div>
</div>
Expand Down Expand Up @@ -267,7 +261,7 @@ export default async function HomePage() {
].map((c) => (
<RevealItem key={c.title}>
<div className="h-full rounded-xl border border-line surface-lit p-6">
<c.icon className="h-5 w-5 text-accent" strokeWidth={2} />
<c.icon className="h-5 w-5 text-muted" strokeWidth={2} />
<h3 className="mt-4 font-medium text-ink">{c.title}</h3>
<p className="mt-2 text-sm leading-relaxed text-muted">{c.body}</p>
</div>
Expand All @@ -276,7 +270,7 @@ export default async function HomePage() {
</RevealGroup>

<Reveal delay={0.1}>
<div className="mt-12 flex flex-col items-start justify-between gap-5 rounded-2xl border border-line bg-gradient-to-br from-surface-2 to-surface p-8 sm:flex-row sm:items-center">
<div className="mt-12 flex flex-col items-start justify-between gap-5 rounded-2xl border border-line bg-surface-2 p-8 sm:flex-row sm:items-center">
<div>
<h3 className="font-display text-xl font-semibold text-ink">
See where the protocols stand
Expand All @@ -288,15 +282,15 @@ export default async function HomePage() {
<div className="flex gap-3">
<Link
href="/registry"
className="inline-flex items-center gap-2 rounded-lg bg-gradient-to-r from-accent to-accent-2 px-5 py-2.5 text-sm font-semibold text-bg transition-transform hover:-translate-y-0.5"
className="inline-flex items-center gap-2 rounded-lg bg-accent px-5 py-2.5 text-sm font-semibold text-bg transition-transform hover:-translate-y-0.5"
>
Open the registry <ArrowRight className="h-4 w-4" />
</Link>
<a
href={GITHUB_URL}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 rounded-lg border border-line bg-surface/60 px-5 py-2.5 text-sm font-medium text-ink hover:border-accent/40"
className="inline-flex items-center gap-2 rounded-lg border border-line-strong px-5 py-2.5 text-sm font-medium text-ink transition-colors hover:border-accent"
>
Contribute an adapter
</a>
Expand Down
19 changes: 9 additions & 10 deletions dashboard/app/about/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { ArrowRight, Bot, GitPullRequest, Radar, ScanEye, ShieldOff } from 'lucide-react';
import { ArrowRight, Bot, GitPullRequest, ScanEye, ShieldOff } from 'lucide-react';
import { GITHUB_URL } from '../lib/site';
import { Reveal, RevealGroup, RevealItem } from '../../components/reveal';

Expand All @@ -14,8 +14,7 @@ export default function AboutPage() {
return (
<div className="mx-auto max-w-3xl px-5 py-14">
<Reveal>
<span className="inline-flex items-center gap-2 rounded-full border border-line bg-surface/60 px-3 py-1 text-xs text-muted">
<Radar className="h-3.5 w-3.5 text-accent" />
<span className="text-xs font-medium uppercase tracking-[0.18em] text-muted">
About Stenion
</span>
<h1 className="mt-5 font-display text-4xl font-semibold tracking-tight text-ink">
Expand All @@ -35,15 +34,15 @@ export default function AboutPage() {
</p>
<ul className="mt-4 space-y-3">
<li className="flex gap-3">
<span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-accent" />
<span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-faint" />
<span>
<strong className="text-ink">Static audits</strong> confirm the code was sound on a
particular date. They say nothing about the pool becoming 95% concentrated in one
asset, or an oracle drifting stale, weeks later.
</span>
</li>
<li className="flex gap-3">
<span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-accent" />
<span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-faint" />
<span>
<strong className="text-ink">TVL trackers</strong> (DeFiLlama already does this well
for Stellar) measure how much is at stake — not how risky the position is. Size is not
Expand Down Expand Up @@ -71,7 +70,7 @@ export default function AboutPage() {
Adapters read data directly from Soroban RPC and Horizon — official, trustless Stellar
infrastructure — so nothing is self-reported by the protocols being scored. The exact
formulas are public in the{' '}
<Link href="/methodology" className="text-accent hover:underline">
<Link href="/methodology" className="text-accent-ink hover:underline">
methodology
</Link>
, and they&apos;re meant to be challenged.
Expand Down Expand Up @@ -111,7 +110,7 @@ export default function AboutPage() {
].map((c) => (
<RevealItem key={c.title}>
<div className="h-full rounded-xl border border-line surface-lit p-6">
<c.icon className="h-5 w-5 text-accent" strokeWidth={2} />
<c.icon className="h-5 w-5 text-muted" strokeWidth={2} />
<h3 className="mt-4 font-medium text-ink">{c.title}</h3>
<p className="mt-2 text-sm leading-relaxed text-muted">{c.body}</p>
</div>
Expand All @@ -135,7 +134,7 @@ export default function AboutPage() {
</Section>

<Reveal>
<div className="mt-14 flex flex-col items-start justify-between gap-5 rounded-2xl border border-line bg-gradient-to-br from-surface-2 to-surface p-8 sm:flex-row sm:items-center">
<div className="mt-14 flex flex-col items-start justify-between gap-5 rounded-2xl border border-line bg-surface-2 p-8 sm:flex-row sm:items-center">
<div>
<h3 className="font-display text-xl font-semibold text-ink">See it working</h3>
<p className="mt-1 text-sm text-muted">
Expand All @@ -145,15 +144,15 @@ export default function AboutPage() {
<div className="flex gap-3">
<Link
href="/registry"
className="inline-flex items-center gap-2 rounded-lg bg-gradient-to-r from-accent to-accent-2 px-5 py-2.5 text-sm font-semibold text-bg transition-transform hover:-translate-y-0.5"
className="inline-flex items-center gap-2 rounded-lg bg-accent px-5 py-2.5 text-sm font-semibold text-bg transition-transform hover:-translate-y-0.5"
>
Open the registry <ArrowRight className="h-4 w-4" />
</Link>
<a
href={GITHUB_URL}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 rounded-lg border border-line bg-surface/60 px-5 py-2.5 text-sm font-medium text-ink hover:border-accent/40"
className="inline-flex items-center gap-2 rounded-lg border border-line-strong px-5 py-2.5 text-sm font-medium text-ink transition-colors hover:border-accent"
>
View on GitHub
</a>
Expand Down
Loading
Loading