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
2 changes: 1 addition & 1 deletion public/feed.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<link>https://usewraith.xyz/blog</link>
<description>Notes on stealth payments, private infrastructure, and the Wraith ecosystem.</description>
<language>en-us</language>
<lastBuildDate>Thu, 30 Jul 2026 09:40:05 GMT</lastBuildDate>
<lastBuildDate>Thu, 30 Jul 2026 11:32:01 GMT</lastBuildDate>
<atom:link href="https://usewraith.xyz/feed.xml" rel="self" type="application/rss+xml" />

<item>
Expand Down
9 changes: 9 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const Privacy = lazy(() => import('./pages/Privacy'));
const UseCases = lazy(() => import('./pages/UseCases'));
const Stellar = lazy(() => import('./pages/Stellar'));
const Roadmap = lazy(() => import('./pages/Roadmap'));
const Grants = lazy(() => import('./pages/Grants'));
const CaseStudies = lazy(() => import('./pages/CaseStudies'));
const Careers = lazy(() => import('./pages/Careers'));
const About = lazy(() => import('./pages/About'));
Expand Down Expand Up @@ -94,6 +95,14 @@ export default function App() {
</Layout>
}
/>
<Route
path="/grants"
element={
<Layout>
<Grants />
</Layout>
}
/>
<Route
path="/about"
element={
Expand Down
174 changes: 174 additions & 0 deletions src/__tests__/grants.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it } from 'vitest';
import App from '../App';
import waveData from '../data/wave.json';

describe('Grants page', () => {
it('renders the grants page for the /grants route', async () => {
window.history.replaceState({}, '', '/grants');

render(<App />);

expect(
await screen.findByRole('heading', { level: 1, name: /build private payments/i }),
).toBeInTheDocument();
});

it('renders the current wave section with name and budget', async () => {
window.history.replaceState({}, '', '/grants');

render(<App />);

const currentWave = waveData.currentWave;
if (currentWave) {
expect(
await screen.findByRole('heading', { level: 2, name: currentWave.name }),
).toBeInTheDocument();
expect(screen.getByText(currentWave.budget)).toBeInTheDocument();
expect(screen.getAllByText(currentWave.fundingSource).length).toBeGreaterThanOrEqual(1);
}
});

it('renders the apply button with correct URL when wave is open', async () => {
window.history.replaceState({}, '', '/grants');

render(<App />);

const currentWave = waveData.currentWave;
if (currentWave?.status === 'open') {
const applyLink = await screen.findByRole('link', { name: /apply on drips/i });
expect(applyLink).toHaveAttribute('href', currentWave.applyUrl);
expect(applyLink).toHaveAttribute('target', '_blank');
}
});

it('renders eligibility and review criteria lists', async () => {
window.history.replaceState({}, '', '/grants');

render(<App />);

const currentWave = waveData.currentWave;
if (currentWave) {
await screen.findByRole('heading', { level: 3, name: /eligibility/i });
expect(
screen.getByRole('heading', { level: 3, name: /review criteria/i }),
).toBeInTheDocument();

for (const item of currentWave.eligibility) {
expect(screen.getByText(item)).toBeInTheDocument();
}

for (const item of currentWave.reviewCriteria) {
expect(screen.getByText(item)).toBeInTheDocument();
}
}
});

it('renders past waves section when past waves exist', async () => {
window.history.replaceState({}, '', '/grants');

render(<App />);

const pastWaves = waveData.pastWaves;
if (pastWaves.length > 0) {
expect(await screen.findByText(/past waves/i)).toBeInTheDocument();

for (const wave of pastWaves) {
expect(screen.getByRole('heading', { level: 2, name: wave.name })).toBeInTheDocument();
expect(screen.getByText((content) => content.startsWith(wave.budget))).toBeInTheDocument();
}
}
});

it('renders past wave recipients when present', async () => {
window.history.replaceState({}, '', '/grants');

render(<App />);

const pastWaves = waveData.pastWaves;
if (pastWaves.length > 0) {
for (const wave of pastWaves) {
if (wave.recipients && wave.recipients.length > 0) {
await screen.findByText(/recipients/i);
for (const r of wave.recipients) {
expect(screen.getByText(r.name)).toBeInTheDocument();
expect(screen.getByText(r.project)).toBeInTheDocument();
expect(screen.getByText(r.grantAmount)).toBeInTheDocument();
}
}
}
}
});

it('renders the FAQ section with all entries', async () => {
window.history.replaceState({}, '', '/grants');

render(<App />);

expect(
await screen.findByRole('heading', { level: 2, name: /frequently asked questions/i }),
).toBeInTheDocument();

const faqEntries = waveData.faq;
for (const entry of faqEntries) {
expect(screen.getByText(entry.question)).toBeInTheDocument();
}
});

it('toggles FAQ answers open and closed', async () => {
window.history.replaceState({}, '', '/grants');
const user = userEvent.setup();

render(<App />);

const faqEntries = waveData.faq;
if (faqEntries.length > 0) {
const firstQuestion = faqEntries[0];
const toggleButton = await screen.findByRole('button', {
name: new RegExp(firstQuestion?.question ?? ''),
});

await user.click(toggleButton);
expect(screen.getByText(firstQuestion?.answer ?? '')).toBeInTheDocument();

await user.click(toggleButton);
expect(screen.queryByText(firstQuestion?.answer ?? '')).not.toBeInTheDocument();
}
});

it('shows Drips as a link in the hero section', async () => {
window.history.replaceState({}, '', '/grants');

render(<App />);

const dripsLinks = await screen.findAllByRole('link', { name: /drips/i });
const dripsLink = dripsLinks.find(
(l) => l.getAttribute('href') === 'https://www.drips.network',
);
expect(dripsLink).toBeDefined();
expect(dripsLink).toHaveAttribute('target', '_blank');
});

it('renders the layout header with brand link back home', async () => {
window.history.replaceState({}, '', '/grants');

render(<App />);

const brandLink = await screen.findByRole('link', { name: /^wraith$/i });
expect(brandLink).toHaveAttribute('href', 'https://usewraith.xyz');
});

it('has no axe violations on the grants page', async () => {
const { axe } = await import('vitest-axe');
window.history.replaceState({}, '', '/grants');

const { container } = render(<App />);

await screen.findByRole('heading', { level: 1, name: /build private payments/i });

const results = await axe(container);

expect(results.violations).toEqual([]);
});
});
13 changes: 13 additions & 0 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ export default function Header() {
>
{t('header.nav.useCases')}
</a>
<Link
to="/grants"
className="font-body text-[13px] text-outline transition-colors duration-150 hover:text-on-surface-variant"
>
{t('header.nav.grants')}
</Link>
<Link
to="/stellar"
className="font-body text-[13px] text-outline transition-colors duration-150 hover:text-on-surface-variant"
Expand Down Expand Up @@ -262,6 +268,13 @@ export default function Header() {
>
{t('header.nav.useCases')}
</a>
<Link
to="/grants"
onClick={closeMenu}
className="font-body text-[13px] text-outline transition-colors duration-150 hover:text-on-surface-variant"
>
{t('header.nav.grants')}
</Link>
<Link
to="/stellar"
onClick={closeMenu}
Expand Down
82 changes: 82 additions & 0 deletions src/data/wave.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"currentWave": {
"id": "wave-1",
"name": "Wave 1: Stealth Address Infrastructure",
"status": "open",
"openDate": "2025-06-01",
"closeDate": "2025-08-31",
"budget": "$150,000",
"fundingSource": "Drips + External",
"description": "Funding for teams building stealth address infrastructure, SDK integrations, and developer tooling. If you are building privacy-preserving payment rails, stealth-address middleware, or tooling that makes unlinkable payments easier to ship, this wave is for you.",
"howToApply": "Submit your proposal through the Drips platform. Include a clear scope, timeline, and budget breakdown with defined milestones.",
"applyUrl": "https://drips.network/grants/wraith/wave-1",
"rewardRange": "$5,000 – $50,000",
"eligibility": [
"Open to individual developers and teams",
"Projects must be open source",
"Must be building on or integrating with Wraith Protocol",
"Previous experience with blockchain development preferred"
],
"reviewCriteria": [
"Technical feasibility and clarity of scope",
"Impact on the Wraith ecosystem",
"Team's experience and track record",
"Alignment with Wraith's privacy mission",
"Realistic timeline and budget planning"
]
},
"pastWaves": [
{
"id": "wave-0",
"name": "Wave 0: Protocol Foundation",
"status": "closed",
"budget": "$75,000",
"fundingSource": "External",
"highlights": [
"Funded the initial SDK development",
"Supported stealth address implementation for EVM chains",
"Granted documentation and tutorial creation"
],
"recipients": [
{ "name": "Stealth Labs", "project": "EVM Stealth Address SDK", "grantAmount": "$25,000" }
]
}
],
"faq": [
{
"id": "what-is-drips",
"question": "What is Drips?",
"answer": "Drips is a protocol for recurring funding of open-source projects. Wraith grants are managed through Drips, which provides transparent, automated fund distribution on a milestone basis."
},
{
"id": "how-is-grant-funded",
"question": "How is the grant funded?",
"answer": "Each wave is funded through a combination of Drips recurring revenue and external contributions from ecosystem partners and the Wraith treasury. The funding source is listed per wave."
},
{
"id": "who-can-apply",
"question": "Who can apply?",
"answer": "Any developer or team building privacy-preserving payment infrastructure, stealth address tooling, or complementary SDK integrations. Both individual contributors and established teams are welcome."
},
{
"id": "how-are-grants-reviewed",
"question": "How are grants reviewed?",
"answer": "Proposals are reviewed by the Wraith core team based on technical merit, ecosystem impact, team capability, and alignment with Wraith's privacy mission. We aim to respond within two weeks of submission."
},
{
"id": "what-is-timeline",
"question": "What is the timeline?",
"answer": "Each wave runs for a fixed period. Applicants are notified of decisions within two weeks of the wave closing. Grant distributions are made through Drips on a milestone basis."
},
{
"id": "are-milestones-required",
"question": "Are milestones required?",
"answer": "Yes. Grants are distributed based on milestone completion. Each proposal should define clear, verifiable deliverables with associated funding tranches."
},
{
"id": "can-i-apply-multiple-waves",
"question": "Can I apply to multiple waves?",
"answer": "Yes. Unfunded proposals from previous waves are welcome to reapply. Past grantees can also apply for follow-up grants in subsequent waves."
}
]
}
3 changes: 2 additions & 1 deletion src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"console": "Console",
"press": "Press",
"useCases": "Use Cases",
"roadmap": "Roadmap"
"roadmap": "Roadmap",
"grants": "Grants"
},
"launchApp": "Launch App",
"github": "GitHub ↗",
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"console": "Consola",
"press": "Prensa",
"useCases": "Casos de uso",
"roadmap": "Hoja de ruta"
"roadmap": "Hoja de ruta",
"grants": "Subvenciones"
},
"launchApp": "Abrir app",
"github": "GitHub ↗",
Expand Down
Loading
Loading