Task #5: Lightning-gated node bootstrap (proof-of-concept)

Pay a Lightning invoice → Timmy auto-provisions a Bitcoin full node on DO.

New: lib/db/src/schema/bootstrap-jobs.ts
- bootstrap_jobs table: id, state, amountSats, paymentHash, paymentRequest,
  dropletId, nodeIp, tailscaleHostname, lnbitsUrl, sshPrivateKey,
  sshKeyDelivered (bool), errorMessage, createdAt, updatedAt
- States: awaiting_payment | provisioning | ready | failed
- Payment data stored inline (no FK to jobs/invoices tables — separate entity)
- db:push applied to create table in Postgres

New: artifacts/api-server/src/lib/provisioner.ts
- ProvisionerService: stubs when DO_API_TOKEN absent, real otherwise
- Stub mode: generates a real RSA 4096-bit SSH keypair via ssh-keygen,
  returns RFC 5737 test IP + fake Tailscale hostname after 2s delay
- Real mode: upload SSH public key to DO → generate Tailscale auth key →
  create DO droplet with cloud-init user_data → poll for public IP (2 min)
- buildCloudInitScript(): non-interactive bash that installs Docker + Tailscale
  + UFW + Bitcoin Knots via docker-compose; joins Tailscale if authkey provided
- provision() designed as fire-and-forget (void); updates DB to ready/failed

New: artifacts/api-server/src/routes/bootstrap.ts
- POST /api/bootstrap: create job + LNbits invoice, return paymentRequest
- GET /api/bootstrap/🆔 poll-driven state machine
  * awaiting_payment: checks payment, fires provisioner on confirm
  * provisioning: returns progress message
  * ready: delivers credentials; SSH private key delivered once then cleared
  * failed: returns error message
- Stub mode message includes the exact /dev/stub/pay URL for easy testing
- nextSteps array guides user through post-provision setup

Updated: artifacts/api-server/src/lib/pricing.ts
- Added bootstrapFee field reading BOOTSTRAP_FEE_SATS env var (default 10000)
- calculateBootstrapFeeSats() method

Updated: artifacts/api-server/src/routes/index.ts
- Mounts bootstrapRouter

Updated: replit.md
- Documents all 7 new env vars (DO_API_TOKEN, DO_REGION, DO_SIZE, etc.)
- Full curl-based flow example with annotated response shape

End-to-end verified in stub mode: POST → pay → provisioning → ready (SSH key)
→ second GET clears key and shows sshKeyNote
This commit is contained in:
alexpaynex
2026-03-18 18:47:48 +00:00
parent 1a60363b74
commit f43e782c50
7 changed files with 617 additions and 0 deletions

View File

@@ -5,6 +5,7 @@ export interface PricingConfig {
workFeeLongSats?: number;
shortMaxChars?: number;
mediumMaxChars?: number;
bootstrapFeeSats?: number;
}
export class PricingService {
@@ -14,6 +15,7 @@ export class PricingService {
private readonly workFeeLong: number;
private readonly shortMax: number;
private readonly mediumMax: number;
private readonly bootstrapFee: number;
constructor(config?: PricingConfig) {
this.evalFee = config?.evalFeeSats ?? 10;
@@ -22,6 +24,9 @@ export class PricingService {
this.workFeeLong = config?.workFeeLongSats ?? 250;
this.shortMax = config?.shortMaxChars ?? 100;
this.mediumMax = config?.mediumMaxChars ?? 300;
this.bootstrapFee =
config?.bootstrapFeeSats ??
(process.env.BOOTSTRAP_FEE_SATS ? parseInt(process.env.BOOTSTRAP_FEE_SATS, 10) : 10_000);
}
calculateEvalFeeSats(): number {
@@ -34,6 +39,10 @@ export class PricingService {
if (len <= this.mediumMax) return this.workFeeMedium;
return this.workFeeLong;
}
calculateBootstrapFeeSats(): number {
return this.bootstrapFee;
}
}
export const pricingService = new PricingService();