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 frontend/src/components/openclaw/assistant-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function StreamResult({ data }: { data: any }) {
return (
<div className="space-y-2 text-xs">
<div className="flex items-center justify-between">
<span className="font-medium text-zinc-200">Stream #{data.id}</span>
<span className="font-medium text-zinc-200">Stream #{data.streamId ?? data.id}</span>
<span className={cn("font-medium", getStreamStatusColor(data.status?.code ?? data.status))}>
{data.status?.label ?? getStreamStatusLabel(data.status)}
</span>
Expand Down
16 changes: 16 additions & 0 deletions openclaw-service/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions openclaw-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express-rate-limit": "^7.5.1",
"zod": "^3.24.2"
},
"devDependencies": {
Expand Down
7 changes: 5 additions & 2 deletions openclaw-service/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export const STREAM_STATUS = {
DEPLETED: 3,
} as const;

export const BLOCKS_PER_DAY = 144;
export const BLOCKS_PER_MONTH = 4320;
// Nakamoto (Epoch 3.0+) cadence: Stacks blocks advance ~every 5s, so a day is
// 17_280 blocks, not the pre-Nakamoto 144. Kept in sync with the frontend.
export const BLOCK_TIME_SECONDS = 5;
export const BLOCKS_PER_DAY = 17_280;
export const BLOCKS_PER_MONTH = 518_400;
export const MAX_STREAMS_PER_USER = 100;
22 changes: 21 additions & 1 deletion openclaw-service/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import express from "express";
import cors from "cors";
import rateLimit from "express-rate-limit";
import { PORT } from "./config";
import { bigIntReplacer } from "./utils";
import { errorHandler } from "./middleware/error-handler";
Expand All @@ -12,17 +13,36 @@ import transactionsRouter from "./routes/transactions";

const app = express();

// Railway terminates TLS at a proxy in front of us, so trust the first hop.
// Without this, express-rate-limit can't read the real client IP from
// X-Forwarded-For and would bucket every request under the proxy's address.
app.set("trust proxy", 1);

// BigInt JSON serialization
app.set("json replacer", bigIntReplacer);

app.use(cors());
app.use(express.json());

// Health check
// Health check — kept outside the rate limiter so Railway's probe is never throttled
app.get("/health", (_req, res) => {
res.json({ status: "ok", service: "stackstream-openclaw" });
});

// Rate limit the API. Every endpoint proxies to Hiro read-only calls, so an
// unthrottled client could burn our upstream quota. 60 requests/min per IP is
// comfortably above normal widget usage while capping abuse.
app.use(
"/api",
rateLimit({
windowMs: 60 * 1000,
limit: 60,
standardHeaders: true,
legacyHeaders: false,
message: { error: "Too many requests, please slow down." },
})
);

// Routes
app.use("/api/streams", streamsRouter);
app.use("/api/daos", daosRouter);
Expand Down
17 changes: 13 additions & 4 deletions openclaw-service/src/stacks-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ export async function getStream(streamId: number): Promise<StreamData | null> {
[uintCV(streamId)]
);
if (result.value === null) return null;
return parseStreamData(result.value);
// cvToJSON nests an (optional (tuple ...)) as { value: { value: {fields} } }
// — the tuple fields live one level below the optional's unwrapped value.
return parseStreamData(result.value.value);
}

export async function getStreamStatus(streamId: number): Promise<number | null> {
Expand Down Expand Up @@ -214,7 +216,8 @@ export async function getDao(admin: string): Promise<DaoData | null> {
[principalCV(admin)]
);
if (result.value === null) return null;
return parseDaoData(result.value);
// Same optional-of-tuple nesting as get-stream (see getStream above).
return parseDaoData(result.value.value);
}

export async function getDaoCount(): Promise<number> {
Expand Down Expand Up @@ -244,8 +247,14 @@ export async function getTokenBalance(
);
const data: any = await res.json();
const ftBalances = data.fungible_tokens || {};
const key = `${tokenContract}::mock-sbtc`;
return BigInt(ftBalances[key]?.balance ?? "0");
// Balances are keyed by `<contractId>::<asset-name>`, and the asset name
// varies per token (sbtc, usda, wrapped-bitcoin, ...). Match by contract
// prefix so this works for any SIP-010 token, not just the testnet mock.
const entry = Object.entries(ftBalances).find(([k]) =>
k.startsWith(`${tokenContract}::`)
);
const balance = (entry?.[1] as { balance?: string } | undefined)?.balance;
return BigInt(balance ?? "0");
}

// ============================================================================
Expand Down
4 changes: 2 additions & 2 deletions openclaw-service/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ export function getStreamStatusLabel(status: number): string {
}
}

/** Estimate time remaining from blocks (Stacks ~10 min/block) */
/** Estimate time remaining from blocks (Nakamoto ~5s/block) */
export function blocksToTimeString(blocks: number): string {
const minutes = blocks * 10;
const minutes = Math.round((blocks * 5) / 60);
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ${minutes % 60}m`;
Expand Down
Loading