feat: THE CREATOR god-tier character, bitcoin choreographies, omni-morph, cameos

- Guy Fawkes mask archetype with golden outline, bitcoin chest symbol, laptop, tier-gated effects
- 12 custom bitcoin-themed choreographies (8 regular + 4 exclusive ultimates)
- Omni-morph system: creator morphs into any of 80+ archetypes instead of cycling 3
- 6% per-round cameo in non-creator fights — drops golden ₿ gifts to fighters
- Custom golden code rain entrance with persistent orbiting particles
- pickChoreography: 50% ultimate chance (100% on crits), all tier ultimates + 4 exclusive
- Server auto-assigns the_creator archetype on login/register for creator pubkey
- FightViewer, payments, queue, orchestrator improvements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 12:08:18 +00:00
co-authored by Claude Opus 4.6
parent 8a3480e5ab
commit 6d390f69b2
14 changed files with 1513 additions and 104 deletions
+49 -8
View File
@@ -4,6 +4,7 @@ import { db, schema } from '../db/index.js'
import { eq } from 'drizzle-orm'
import { createEntryInvoice, checkPaymentStatus, redeemCashuToken } from '../engine/payments.js'
import { encrypt, decrypt } from '../engine/crypto.js'
import { rateLimit } from '../middleware/rate-limit.js'
export const paymentsRouter = new Hono()
@@ -82,11 +83,23 @@ paymentsRouter.get('/wallet-status', async (c) => {
return c.json({ connected: true, method: walletRows[0].method })
})
// POST /create-invoice
paymentsRouter.post('/create-invoice', async (c) => {
const { botId } = await c.req.json<{ botId: string }>()
// POST /create-invoice — rate limited: 10 per minute per IP
paymentsRouter.post('/create-invoice', rateLimit(60_000, 10), async (c) => {
const { botId, pubkey } = await c.req.json<{ botId: string; pubkey?: string }>()
if (!botId) return c.json({ error: 'Missing botId' }, 400)
// In production, verify bot ownership
if (process.env.NODE_ENV === 'production') {
if (!pubkey || typeof pubkey !== 'string' || pubkey.length !== 64) {
return c.json({ error: 'Missing pubkey' }, 400)
}
const botRows = await db.select({ publicKey: schema.bots.publicKey })
.from(schema.bots).where(eq(schema.bots.id, botId)).limit(1)
if (botRows.length === 0 || botRows[0].publicKey !== pubkey) {
return c.json({ error: 'Unauthorized' }, 403)
}
}
try {
const result = await createEntryInvoice(botId)
return c.json(result)
@@ -96,9 +109,12 @@ paymentsRouter.post('/create-invoice', async (c) => {
}
})
// GET /check/:paymentId
paymentsRouter.get('/check/:paymentId', async (c) => {
// GET /check/:paymentId — rate limited: 30 per minute per IP
paymentsRouter.get('/check/:paymentId', rateLimit(60_000, 30), async (c) => {
const paymentId = c.req.param('paymentId')
if (!paymentId || paymentId.length > 24) {
return c.json({ error: 'Invalid paymentId' }, 400)
}
try {
const status = await checkPaymentStatus(paymentId)
return c.json({ status })
@@ -109,8 +125,12 @@ paymentsRouter.get('/check/:paymentId', async (c) => {
})
// POST /confirm/:paymentId — frontend confirms after NWC pay returns preimage
paymentsRouter.post('/confirm/:paymentId', async (c) => {
paymentsRouter.post('/confirm/:paymentId', rateLimit(60_000, 20), async (c) => {
const paymentId = c.req.param('paymentId')
if (!paymentId || paymentId.length > 24) {
return c.json({ error: 'Invalid paymentId' }, 400)
}
const { preimage, pubkey } = await c.req.json<{ preimage?: string; pubkey?: string }>().catch(() => ({ preimage: undefined, pubkey: undefined }))
const rows = await db.select().from(schema.payments)
@@ -119,16 +139,37 @@ paymentsRouter.post('/confirm/:paymentId', async (c) => {
const payment = rows[0]
if (payment.status === 'confirmed') return c.json({ status: 'confirmed' })
if (payment.status !== 'pending') return c.json({ error: 'Payment is not pending' }, 400)
// Must be an inbound entry payment
if (payment.direction !== 'in') return c.json({ error: 'Cannot confirm outbound payments' }, 400)
// Verify caller owns this payment's bot
if (pubkey) {
if (pubkey && typeof pubkey === 'string' && pubkey.length === 64) {
const botRows = await db.select({ publicKey: schema.bots.publicKey })
.from(schema.bots).where(eq(schema.bots.id, payment.botId)).limit(1)
if (botRows.length === 0 || botRows[0].publicKey !== pubkey) {
return c.json({ error: 'Unauthorized' }, 403)
}
} else if (process.env.NODE_ENV === 'production') {
return c.json({ error: 'Missing pubkey' }, 400)
return c.json({ error: 'Missing or invalid pubkey' }, 400)
}
// In production, also verify payment via NWC lookup (belt and suspenders)
if (process.env.NODE_ENV === 'production' && payment.invoice && payment.invoice !== 'dev_auto_confirmed') {
try {
const serverStatus = await checkPaymentStatus(paymentId)
if (serverStatus !== 'confirmed') {
return c.json({ error: 'Server could not verify payment. Try again.' }, 402)
}
// checkPaymentStatus already updated the DB
return c.json({ status: 'confirmed' })
} catch {
// NWC check failed — fall through to client-confirmed path with preimage
if (!preimage) {
return c.json({ error: 'Payment verification failed and no preimage provided.' }, 402)
}
}
}
await db.update(schema.payments).set({