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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ grant-application/DOMAIN_SETUP.md
grant-application/TESTER_GUIDE.md
grant-application/TEAM_UPDATE_EMAIL.md
grant-application/DEMO_VIDEO_SCRIPT.md
grant-application/OUTREACH_MESSAGES_JUL2026.md
3 changes: 2 additions & 1 deletion frontend/.env.production
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
NEXT_PUBLIC_NETWORK=mainnet
NEXT_PUBLIC_CONTRACT_DEPLOYER=SP2V6TCRFTYQHP8F4D9HSFZHRQNGVBQEZR0TMSM79
NEXT_PUBLIC_OPENCLAW_API_URL=https://stackstream-production.up.railway.app
# OpenClaw API is served by this app's own route handlers (same origin).
# Leave unset. Only set this to point at an external service (e.g. Railway).
17 changes: 17 additions & 0 deletions frontend/src/app/api/blocks/current/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {
getCurrentBlockHeight,
jsonResponse,
errorResponse,
} from "@/lib/openclaw-server";

export const dynamic = "force-dynamic";

// GET /api/blocks/current — current Stacks block height
export async function GET() {
try {
const height = await getCurrentBlockHeight();
return jsonResponse({ blockHeight: height });
} catch (err) {
return errorResponse(err);
}
}
32 changes: 32 additions & 0 deletions frontend/src/app/api/daos/[admin]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {
getDao,
formatTokenAmount,
jsonResponse,
errorResponse,
STACKS_ADDRESS_RE,
} from "@/lib/openclaw-server";

export const dynamic = "force-dynamic";

// GET /api/daos/:admin — DAO info by admin address
export async function GET(
_req: Request,
{ params }: { params: Promise<{ admin: string }> }
) {
try {
const { admin } = await params;
if (!STACKS_ADDRESS_RE.test(admin)) {
return jsonResponse({ error: "Invalid Stacks address" }, 400);
}
const dao = await getDao(admin);
if (!dao) {
return jsonResponse({ error: "DAO not found" }, 404);
}
return jsonResponse({
...dao,
totalDepositedFormatted: formatTokenAmount(dao.totalDeposited),
});
} catch (err) {
return errorResponse(err);
}
}
52 changes: 52 additions & 0 deletions frontend/src/app/api/stats/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
getStreamNonce,
getDaoCount,
getCurrentBlockHeight,
getNetwork,
jsonResponse,
errorResponse,
} from "@/lib/openclaw-server";

export const dynamic = "force-dynamic";

interface StatsSnapshot {
network: string;
blockHeight: number;
streamsCreated: number;
workspacesRegistered: number;
asOf: string;
}

// The north-star number changes at most once per new stream, so a short cache
// keeps repeated polls from burning Hiro quota. Module state persists per warm
// serverless instance, which is enough — a cold start just refetches.
const CACHE_TTL_MS = 60_000;
let cached: { snapshot: StatsSnapshot; expires: number } | null = null;

// GET /api/stats — protocol usage counters, all verifiable on-chain.
// streamsCreated mirrors stream-manager's get-stream-nonce (total streams ever
// opened), the project's north-star growth metric.
export async function GET() {
try {
if (cached && Date.now() < cached.expires) {
return jsonResponse(cached.snapshot);
}
const [streamsCreated, workspacesRegistered, blockHeight] =
await Promise.all([
getStreamNonce(),
getDaoCount(),
getCurrentBlockHeight(),
]);
const snapshot: StatsSnapshot = {
network: getNetwork(),
blockHeight,
streamsCreated,
workspacesRegistered,
asOf: new Date().toISOString(),
};
cached = { snapshot, expires: Date.now() + CACHE_TTL_MS };
return jsonResponse(snapshot);
} catch (err) {
return errorResponse(err);
}
}
68 changes: 68 additions & 0 deletions frontend/src/app/api/streams/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
getStream,
getClaimableBalance,
getStreamedAmount,
getRemainingBalance,
getRefundableAmount,
getCurrentBlockHeight,
getStreamStatusLabel,
getStreamProgress,
formatTokenAmount,
jsonResponse,
errorResponse,
STREAM_ID_RE,
} from "@/lib/openclaw-server";

export const dynamic = "force-dynamic";

// GET /api/streams/:id — full stream data with computed fields
export async function GET(
_req: Request,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id: rawId } = await params;
if (!STREAM_ID_RE.test(rawId)) {
return jsonResponse({ error: "Invalid stream id" }, 400);
}
const id = Number(rawId);

const stream = await getStream(id);
if (!stream) {
return jsonResponse({ error: "Stream not found" }, 404);
}

const [claimable, streamed, remaining, refundable, currentBlock] =
await Promise.all([
getClaimableBalance(id),
getStreamedAmount(id),
getRemainingBalance(id),
getRefundableAmount(id),
getCurrentBlockHeight(),
]);

const progress = getStreamProgress(
stream.startBlock,
stream.endBlock,
currentBlock,
stream.totalPausedDuration
);

return jsonResponse({
streamId: id,
...stream,
statusLabel: getStreamStatusLabel(stream.status),
claimable,
streamed,
remaining,
refundable,
currentBlock,
progress: Math.round(progress * 100) / 100,
depositFormatted: formatTokenAmount(stream.depositAmount),
claimableFormatted:
claimable !== null ? formatTokenAmount(claimable) : null,
});
} catch (err) {
return errorResponse(err);
}
}
25 changes: 25 additions & 0 deletions frontend/src/app/api/streams/recipient/[address]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {
getRecipientStreams,
jsonResponse,
errorResponse,
STACKS_ADDRESS_RE,
} from "@/lib/openclaw-server";

export const dynamic = "force-dynamic";

// GET /api/streams/recipient/:address — streams where address is recipient
export async function GET(
_req: Request,
{ params }: { params: Promise<{ address: string }> }
) {
try {
const { address } = await params;
if (!STACKS_ADDRESS_RE.test(address)) {
return jsonResponse({ error: "Invalid Stacks address" }, 400);
}
const ids = await getRecipientStreams(address);
return jsonResponse({ address, streamIds: ids, count: ids.length });
} catch (err) {
return errorResponse(err);
}
}
25 changes: 25 additions & 0 deletions frontend/src/app/api/streams/sender/[address]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {
getSenderStreams,
jsonResponse,
errorResponse,
STACKS_ADDRESS_RE,
} from "@/lib/openclaw-server";

export const dynamic = "force-dynamic";

// GET /api/streams/sender/:address — streams where address is sender
export async function GET(
_req: Request,
{ params }: { params: Promise<{ address: string }> }
) {
try {
const { address } = await params;
if (!STACKS_ADDRESS_RE.test(address)) {
return jsonResponse({ error: "Invalid Stacks address" }, 400);
}
const ids = await getSenderStreams(address);
return jsonResponse({ address, streamIds: ids, count: ids.length });
} catch (err) {
return errorResponse(err);
}
}
7 changes: 4 additions & 3 deletions frontend/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ export const STREAM_FACTORY_CONTRACT = `${CONTRACT_DEPLOYER}.stream-factory`;
export const MOCK_TOKEN_CONTRACT = `${CONTRACT_DEPLOYER}.mock-sip010-token`;
export const SIP010_TRAIT_CONTRACT = `${CONTRACT_DEPLOYER}.sip-010-trait`;

// OpenClaw API
export const OPENCLAW_API_URL =
process.env.NEXT_PUBLIC_OPENCLAW_API_URL ?? "http://localhost:3001";
// OpenClaw API. Empty string = same origin: the API runs as Next.js route
// handlers in this app (src/app/api), so no separate backend is needed.
// Set NEXT_PUBLIC_OPENCLAW_API_URL only to point at an external service.
export const OPENCLAW_API_URL = process.env.NEXT_PUBLIC_OPENCLAW_API_URL ?? "";

// Hiro API
export const HIRO_API_BASE = IS_MAINNET
Expand Down
Loading
Loading