Task #2: MVP Foundation — injectable services, DB schema, smoke test
DB schema
- jobs and invoices tables added to lib/db/src/schema/
- schema barrel updated (jobs, invoices, conversations, messages)
- pnpm --filter @workspace/db run push applied successfully
LNbitsService (artifacts/api-server/src/lib/lnbits.ts)
- Injectable class accepting optional { url, apiKey } config
- Falls back to LNBITS_URL / LNBITS_API_KEY env vars
- Auto-detects stub mode when credentials are absent; logs warning
- createInvoice() -> { paymentHash, paymentRequest }
- checkInvoicePaid() -> boolean
- stubMarkPaid() helper for dev/test flows
- Real LNbits REST v1 calls wired behind the stub guard
AgentService (artifacts/api-server/src/lib/agent.ts)
- Injectable class with configurable evalModel / workModel
- evaluateRequest(text) -> { accepted: boolean, reason: string }
uses claude-haiku-4-5; strips markdown fences before JSON parse
- executeWork(text) -> { result: string } uses claude-sonnet-4-6
- Wired via Replit Anthropic AI Integration (no user API key)
PricingService (artifacts/api-server/src/lib/pricing.ts)
- Injectable class with configurable fee/bucket thresholds
- calculateEvalFeeSats() -> 10 sats (fixed)
- calculateWorkFeeSats(text) -> 50/100/250 by char-length bucket
- Zero LLM involvement; fully deterministic
Smoke test (scripts/src/smoke.ts)
- pnpm --filter @workspace/scripts run smoke
- Verifies LNbits stub: create, check unpaid, mark paid, check paid
- Verifies Anthropic: evaluateRequest round-trip
- Both checks passed
replit.md
- Documented required (LNBITS_URL, LNBITS_API_KEY) and auto-provisioned secrets
- Stub-mode behaviour explained
This commit is contained in:
@@ -5,33 +5,87 @@ export interface LNbitsInvoice {
|
||||
paymentRequest: string;
|
||||
}
|
||||
|
||||
export interface LNbitsInvoiceStatus {
|
||||
paid: boolean;
|
||||
paidAt?: Date;
|
||||
export interface LNbitsConfig {
|
||||
url: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
const paidInvoices = new Set<string>();
|
||||
const stubPaidInvoices = new Set<string>();
|
||||
|
||||
export async function createInvoice(
|
||||
amountSats: number,
|
||||
memo: string,
|
||||
): Promise<LNbitsInvoice> {
|
||||
const paymentHash = randomBytes(32).toString("hex");
|
||||
const paymentRequest = `lnbcrt${amountSats}u1stub_${paymentHash.slice(0, 16)}`;
|
||||
console.log(`[stub] Created invoice: ${amountSats} sats — "${memo}" — hash=${paymentHash}`);
|
||||
return { paymentHash, paymentRequest };
|
||||
}
|
||||
export class LNbitsService {
|
||||
private readonly url: string;
|
||||
private readonly apiKey: string;
|
||||
readonly stubMode: boolean;
|
||||
|
||||
export async function checkInvoicePaid(
|
||||
paymentHash: string,
|
||||
): Promise<LNbitsInvoiceStatus> {
|
||||
if (paidInvoices.has(paymentHash)) {
|
||||
return { paid: true, paidAt: new Date() };
|
||||
constructor(config?: Partial<LNbitsConfig>) {
|
||||
this.url = config?.url ?? process.env.LNBITS_URL ?? "";
|
||||
this.apiKey = config?.apiKey ?? process.env.LNBITS_API_KEY ?? "";
|
||||
this.stubMode = !this.url || !this.apiKey;
|
||||
if (this.stubMode) {
|
||||
console.warn("[LNbitsService] No LNBITS_URL/LNBITS_API_KEY — running in STUB mode. Invoices are simulated.");
|
||||
}
|
||||
}
|
||||
|
||||
async createInvoice(amountSats: number, memo: string): Promise<LNbitsInvoice> {
|
||||
if (this.stubMode) {
|
||||
const paymentHash = randomBytes(32).toString("hex");
|
||||
const paymentRequest = `lnbcrt${amountSats}u1stub_${paymentHash.slice(0, 16)}`;
|
||||
console.log(`[stub] Created invoice: ${amountSats} sats — "${memo}" — hash=${paymentHash}`);
|
||||
return { paymentHash, paymentRequest };
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.url.replace(/\/$/, "")}/api/v1/payments`, {
|
||||
method: "POST",
|
||||
headers: this.headers(),
|
||||
body: JSON.stringify({ out: false, amount: amountSats, memo }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
throw new Error(`LNbits createInvoice failed (${response.status}): ${body}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as {
|
||||
payment_hash: string;
|
||||
payment_request: string;
|
||||
};
|
||||
return { paymentHash: data.payment_hash, paymentRequest: data.payment_request };
|
||||
}
|
||||
|
||||
async checkInvoicePaid(paymentHash: string): Promise<boolean> {
|
||||
if (this.stubMode) {
|
||||
return stubPaidInvoices.has(paymentHash);
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${this.url.replace(/\/$/, "")}/api/v1/payments/${paymentHash}`,
|
||||
{ method: "GET", headers: this.headers() },
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
throw new Error(`LNbits checkInvoice failed (${response.status}): ${body}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { paid: boolean };
|
||||
return data.paid;
|
||||
}
|
||||
|
||||
/** Stub-only helper: mark an invoice as paid for testing/dev flows. */
|
||||
stubMarkPaid(paymentHash: string): void {
|
||||
if (!this.stubMode) {
|
||||
throw new Error("stubMarkPaid called on a real LNbitsService instance");
|
||||
}
|
||||
stubPaidInvoices.add(paymentHash);
|
||||
console.log(`[stub] Marked invoice paid: hash=${paymentHash}`);
|
||||
}
|
||||
|
||||
private headers(): Record<string, string> {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"X-Api-Key": this.apiKey,
|
||||
};
|
||||
}
|
||||
return { paid: false };
|
||||
}
|
||||
|
||||
export function markInvoicePaid(paymentHash: string): void {
|
||||
paidInvoices.add(paymentHash);
|
||||
console.log(`[stub] Marked invoice paid: hash=${paymentHash}`);
|
||||
}
|
||||
export const lnbitsService = new LNbitsService();
|
||||
|
||||
Reference in New Issue
Block a user