Initial commit — federated self-custodial Spark/Lightning tip bot

- grammY bot: /start, /unlock, /tip, /contact, /claim, /settings, /wallet
- AES-256-GCM mnemonic encryption with scrypt key derivation
- In-memory unlock sessions with background sweep
- Atomic claim handling (TOCTOU-safe)
- PIN rate limiting (5 attempts → 15 min lockout)
- Fastify API server + Telegram Mini App (setup, unlock, send, receive, history)
- One-time seed reveal via Mini App or auto-deleted DM message
- Federated registry client
- Docker Compose deployment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
goyban
2026-05-03 13:21:43 +00:00
commit e857bf4ec6
40 changed files with 4689 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
import type { FastifyInstance } from "fastify";
import { validateInitData, AuthError } from "../auth";
import { findUser } from "../../db/users";
import { getDb } from "../../db/schema";
interface TxRow {
id: string;
amount_sats: number;
status: string;
recipient_address: string | null;
recipient_user_id: number | null;
initiated_via: string;
created_at: string;
}
export async function historyRoutes(app: FastifyInstance): Promise<void> {
app.get<{ Querystring: { limit?: string; offset?: string } }>(
"/api/history",
async (req, reply) => {
const initData = req.headers["x-init-data"] as string | undefined;
if (!initData) return reply.status(401).send({ error: "Missing X-Init-Data header" });
let userId: number;
try {
const validated = validateInitData(initData);
userId = validated.user.id;
} catch (err) {
return reply.status(401).send({ error: err instanceof AuthError ? err.message : "Unauthorized" });
}
if (!findUser(userId)) return reply.status(404).send({ error: "Wallet not found" });
const limit = Math.min(parseInt(req.query.limit ?? "20", 10), 100);
const offset = parseInt(req.query.offset ?? "0", 10);
const rows = getDb()
.prepare(
`SELECT id, amount_sats, status, recipient_address, recipient_user_id,
initiated_via, created_at
FROM pending_transactions
WHERE initiator_user_id = ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?`
)
.all(userId, limit, offset) as TxRow[];
return { transactions: rows, limit, offset };
}
);
}