Skip to content
Open
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: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@

# Keyfiles
.keypairs/
.vercel
.env*.local
26 changes: 26 additions & 0 deletions .vercelignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Rust build artifacts
target/
**/target/

# Dependencies (reinstalled by Vercel)
node_modules/
**/node_modules/

# Generated files (rebuilt during Vercel build)
**/dist/
**/generated/

# Not needed for frontend build
program/
clients/rust/
tests/
examples/
.github/

# Local / dev
.env*.local
.DS_Store
keypairs/
.keypairs/
.cus/
test-ledger/
2 changes: 2 additions & 0 deletions apps/web/.env.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
NEXT_PUBLIC_RPC_URL=https://api.devnet.solana.com
NEXT_PUBLIC_PROGRAM_ID=Escrowae7RaUfNn4oEZHywMXE5zWzYCXenwrCDaEoifg
4 changes: 4 additions & 0 deletions apps/web/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.vercel
.next
tsconfig.tsbuildinfo
.env.local
6 changes: 6 additions & 0 deletions apps/web/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import './.next/types/routes.d.ts';

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
11 changes: 11 additions & 0 deletions apps/web/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
transpilePackages: ['@solana/escrow-program-client'],
env: {
NEXT_PUBLIC_RPC_URL: process.env.NEXT_PUBLIC_RPC_URL ?? 'https://api.devnet.solana.com',
NEXT_PUBLIC_PROGRAM_ID: process.env.NEXT_PUBLIC_PROGRAM_ID ?? 'Escrowae7RaUfNn4oEZHywMXE5zWzYCXenwrCDaEoifg',
},
};

export default nextConfig;
38 changes: 38 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@solana/escrow-program-web",
"version": "0.0.1",
"private": true,
"scripts": {
"prebuild": "cd ../.. && pnpm tsx scripts/generate-ts-client.ts && pnpm --filter @solana/escrow-program-client build",
"build": "next build",
"dev": "next dev",
"start": "next start",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@base-ui/react": "^1.1.0",
"@solana-program/system": "^0.12.0",
"@solana-program/token": "^0.12.0",
"@solana-program/token-2022": "^0.9.0",
"@solana/design-system": "^1.0.0",
"@solana/escrow-program-client": "workspace:*",
"@solana/kit": "^6.3.1",
"@solana/wallet-adapter-react": "^0.15.39",
"@solana/wallet-adapter-react-ui": "^0.9.39",
"@solana/wallet-adapter-wallets": "^0.19.37",
"@solana/web3.js": "^1.98.4",
"clsx": "^2.1.1",
"motion": "^12.26.0",
"next": "^16.1.6",
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.2.1",
"@types/node": "^25.5.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"tailwindcss": "^4.2.1",
"typescript": "^5.9.3"
}
}
5 changes: 5 additions & 0 deletions apps/web/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};
28 changes: 28 additions & 0 deletions apps/web/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@import 'tailwindcss';
@import '@solana/design-system/styles';
@import '@solana/design-system/base.css';

:root {
--color-bg: #0f0f0f;
--color-card: #1a1a1a;
--color-border: #2a2a2a;
--color-accent: #a3e635;
--color-text: #f0f0f0;
--color-muted: #888888;
--color-error: #f87171;
--color-success: #4ade80;
}

* {
box-sizing: border-box;
margin: 0;
padding: 0;
}

html,
body {
background-color: var(--color-bg);
color: var(--color-text);
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
min-height: 100vh;
}
19 changes: 19 additions & 0 deletions apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Metadata } from 'next';
import './globals.css';
import '@solana/wallet-adapter-react-ui/styles.css';
import { Providers } from '@/components/Providers';

export const metadata: Metadata = {
title: 'Escrow Program',
description: 'Solana escrow program frontend',
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className="dark">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
173 changes: 173 additions & 0 deletions apps/web/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
'use client';

import { useState } from 'react';
import { Button } from '@solana/design-system/button';
import { WalletButton } from '@/components/WalletButton';
import { RpcBadge } from '@/components/RpcBadge';
import { QuickDefaults } from '@/components/QuickDefaults';
import { RecentTransactions } from '@/components/RecentTransactions';
import { CreateEscrow } from '@/components/instructions/CreateEscrow';
import { UpdateAdmin } from '@/components/instructions/UpdateAdmin';
import { AllowMint } from '@/components/instructions/AllowMint';
import { BlockMint } from '@/components/instructions/BlockMint';
import { AddTimelock } from '@/components/instructions/AddTimelock';
import { SetHook } from '@/components/instructions/SetHook';
import { BlockTokenExtension } from '@/components/instructions/BlockTokenExtension';
import { SetArbiter } from '@/components/instructions/SetArbiter';
import { Deposit } from '@/components/instructions/Deposit';
import { Withdraw } from '@/components/instructions/Withdraw';

type InstructionId =
| 'createEscrow'
| 'updateAdmin'
| 'allowMint'
| 'blockMint'
| 'addTimelock'
| 'setHook'
| 'blockTokenExtension'
| 'setArbiter'
| 'deposit'
| 'withdraw';

const NAV: {
group: string;
items: { id: InstructionId; label: string }[];
}[] = [
{
group: 'ADMIN',
items: [
{ id: 'createEscrow', label: 'Create Escrow' },
{ id: 'updateAdmin', label: 'Update Admin' },
{ id: 'allowMint', label: 'Allow Mint' },
{ id: 'blockMint', label: 'Block Mint' },
],
},
{
group: 'EXTENSIONS',
items: [
{ id: 'addTimelock', label: 'Add Timelock' },
{ id: 'setHook', label: 'Set Hook' },
{ id: 'blockTokenExtension', label: 'Block Token Ext' },
{ id: 'setArbiter', label: 'Set Arbiter' },
],
},
{
group: 'OPERATIONS',
items: [
{ id: 'deposit', label: 'Deposit' },
{ id: 'withdraw', label: 'Withdraw' },
],
},
];

const PANELS: Record<InstructionId, { title: string; component: React.ComponentType }> = {
createEscrow: { title: 'Create Escrow', component: CreateEscrow },
updateAdmin: { title: 'Update Admin', component: UpdateAdmin },
allowMint: { title: 'Allow Mint', component: AllowMint },
blockMint: { title: 'Block Mint', component: BlockMint },
addTimelock: { title: 'Add Timelock', component: AddTimelock },
setHook: { title: 'Set Hook', component: SetHook },
blockTokenExtension: { title: 'Block Token Extension', component: BlockTokenExtension },
setArbiter: { title: 'Set Arbiter', component: SetArbiter },
deposit: { title: 'Deposit', component: Deposit },
withdraw: { title: 'Withdraw', component: Withdraw },
};

export default function HomePage() {
const [active, setActive] = useState<InstructionId>('createEscrow');
const panel = PANELS[active];
const Panel = panel.component;

return (
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
{/* Header */}
<header
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 24px',
borderBottom: '1px solid var(--color-border)',
background: 'var(--color-card)',
position: 'sticky',
top: 0,
zIndex: 10,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<span style={{ fontWeight: 700, fontSize: '1rem', color: 'var(--color-accent)' }}>
Escrow Program
</span>
<RpcBadge />
</div>
<WalletButton />
</header>

{/* Body */}
<div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
{/* Sidebar */}
<nav
style={{
width: 200,
borderRight: '1px solid var(--color-border)',
padding: '16px 0',
flexShrink: 0,
overflowY: 'auto',
}}
>
{NAV.map(({ group, items }) => (
<div key={group} style={{ marginBottom: 24 }}>
<div
style={{
fontSize: '0.6875rem',
fontWeight: 700,
color: 'var(--color-muted)',
letterSpacing: '0.08em',
padding: '0 16px',
marginBottom: 6,
}}
>
{group}
</div>
{items.map(item => (
<Button
key={item.id}
onClick={() => setActive(item.id)}
variant={active === item.id ? 'primary' : 'secondary'}
size="sm"
style={{
width: '100%',
justifyContent: 'flex-start',
borderRadius: 0,
}}
>
{item.label}
</Button>
))}
</div>
))}
</nav>

{/* Main panel */}
<main style={{ flex: 1, padding: '32px 40px', overflowY: 'auto' }}>
<QuickDefaults />
<RecentTransactions />
<h2
style={{
fontSize: '1.125rem',
fontWeight: 600,
marginBottom: 24,
paddingBottom: 16,
borderBottom: '1px solid var(--color-border)',
}}
>
{panel.title}
</h2>
<div style={{ maxWidth: 520 }}>
<Panel />
</div>
</main>
</div>
</div>
);
}
18 changes: 18 additions & 0 deletions apps/web/src/components/Providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client';

import { RpcProvider } from '@/contexts/RpcContext';
import { WalletProvider } from '@/contexts/WalletContext';
import { RecentTransactionsProvider } from '@/contexts/RecentTransactionsContext';
import { SavedValuesProvider } from '@/contexts/SavedValuesContext';

export function Providers({ children }: { children: React.ReactNode }) {
return (
<RpcProvider>
<WalletProvider>
<RecentTransactionsProvider>
<SavedValuesProvider>{children}</SavedValuesProvider>
</RecentTransactionsProvider>
</WalletProvider>
</RpcProvider>
);
}
Loading