feat: v5 — boxing poster fight cards, 12-char names, diverse mock bots
- Fight Card page: dramatic poster background with cross-hatch, spotlights, vignettes, corner brackets, scan lines; 3D VS orb with punch animation; selectable undercard with main event always pinned at top - PosterSprite: high-quality 480px poster frame with 6-pass renderer (aura, glow, bevel, specular, particles); PixelGlove component - 12-char bot name limit across all forms and server validation - Mock bots: all 100 now have diverse archetypes (25 types), 25% human fighters; seedMockBots updates existing bots on restart - Leaderboard: inline SpritePreview next to each bot name - Nostr auth: persistent login, nsec copy button - Wallet: NWC + Lightning Address, ranked fight flow - Server: payments, ranked queue, customization endpoint Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
ccf4196647
commit
f6eb7d2845
@@ -1,8 +1,9 @@
|
||||
import { ref, readonly, computed } from 'vue'
|
||||
import { useNostr } from './useNostr.js'
|
||||
import { finalizeEvent } from 'nostr-tools'
|
||||
import * as nip04 from 'nostr-tools/nip04'
|
||||
import * as nip44 from 'nostr-tools/nip44'
|
||||
import { hexToBytes } from 'nostr-tools/utils'
|
||||
import { hexToBytes, bytesToHex } from 'nostr-tools/utils'
|
||||
|
||||
type WalletMethod = 'nwc' | 'lnaddress' | null
|
||||
type PaymentStatus = 'idle' | 'invoiced' | 'paying' | 'confirmed' | 'failed'
|
||||
@@ -150,14 +151,29 @@ export function useWallet() {
|
||||
const { bolt11, paymentId } = await invoiceRes.json()
|
||||
pendingPayment.value = { paymentId, bolt11 }
|
||||
|
||||
// If NWC connected, auto-pay via NWC
|
||||
// Dev mode: server auto-confirmed, skip payment
|
||||
if (bolt11 === 'dev_auto_confirmed') {
|
||||
paymentStatus.value = 'confirmed'
|
||||
return paymentId
|
||||
}
|
||||
|
||||
// If NWC connected, auto-pay via NWC and confirm directly
|
||||
const nwcUrl = localStorage.getItem('bf_nwc_url')
|
||||
if (nwcUrl) {
|
||||
paymentStatus.value = 'paying'
|
||||
await payViaNWC(nwcUrl, bolt11)
|
||||
const preimage = await payViaNWC(nwcUrl, bolt11)
|
||||
|
||||
// Tell server payment is confirmed (skip lookup_invoice polling)
|
||||
await fetch(`/api/payments/confirm/${paymentId}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ preimage, pubkey: pubkey.value }),
|
||||
})
|
||||
paymentStatus.value = 'confirmed'
|
||||
return paymentId
|
||||
}
|
||||
|
||||
// Poll for confirmation
|
||||
// No NWC — poll for confirmation (manual payment / QR code flow)
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await new Promise(r => setTimeout(r, 2000))
|
||||
const checkRes = await fetch(`/api/payments/check/${paymentId}`)
|
||||
@@ -220,13 +236,13 @@ export function useWallet() {
|
||||
}
|
||||
|
||||
/** Send a pay_invoice request via NWC WebSocket */
|
||||
async function payViaNWC(nwcUrl: string, bolt11: string): Promise<void> {
|
||||
async function payViaNWC(nwcUrl: string, bolt11: string): Promise<string | undefined> {
|
||||
const nwc = parseNwcUrl(nwcUrl)
|
||||
const conversationKey = nip44.v2.utils.getConversationKey(nwc.secret, nwc.pubkey)
|
||||
const secretHex = bytesToHex(nwc.secret)
|
||||
|
||||
const content = nip44.v2.encrypt(
|
||||
// Use NIP-04 encryption (BTCPay/LND compatibility)
|
||||
const content = await nip04.encrypt(secretHex, nwc.pubkey,
|
||||
JSON.stringify({ method: 'pay_invoice', params: { invoice: bolt11 } }),
|
||||
conversationKey,
|
||||
)
|
||||
|
||||
const event = finalizeEvent({
|
||||
@@ -240,41 +256,48 @@ async function payViaNWC(nwcUrl: string, bolt11: string): Promise<void> {
|
||||
const ws = new WebSocket(nwc.relay)
|
||||
const timeout = setTimeout(() => {
|
||||
ws.close()
|
||||
// Don't reject — the server-side poll will catch confirmation
|
||||
resolve()
|
||||
resolve(undefined)
|
||||
}, 30_000)
|
||||
|
||||
ws.onopen = () => {
|
||||
// Subscribe for response
|
||||
const subId = Math.random().toString(36).slice(2, 10)
|
||||
ws.send(JSON.stringify(['REQ', subId, {
|
||||
kinds: [23195],
|
||||
authors: [nwc.pubkey],
|
||||
'#e': [event.id],
|
||||
}]))
|
||||
|
||||
// Publish the payment request
|
||||
ws.send(JSON.stringify(['EVENT', event]))
|
||||
}
|
||||
|
||||
ws.onmessage = (msg) => {
|
||||
ws.onmessage = async (msg) => {
|
||||
try {
|
||||
const data = JSON.parse(msg.data)
|
||||
if (data[0] === 'EVENT' && data[2]?.kind === 23195) {
|
||||
clearTimeout(timeout)
|
||||
ws.close()
|
||||
// Payment sent — server-side poll will confirm
|
||||
resolve()
|
||||
// Decrypt response — try NIP-04 first, fall back to NIP-44
|
||||
let decrypted: string
|
||||
try {
|
||||
decrypted = await nip04.decrypt(secretHex, nwc.pubkey, data[2].content)
|
||||
} catch {
|
||||
const convKey = nip44.v2.utils.getConversationKey(nwc.secret, nwc.pubkey)
|
||||
decrypted = nip44.v2.decrypt(data[2].content, convKey)
|
||||
}
|
||||
const result = JSON.parse(decrypted)
|
||||
if (result.error) {
|
||||
reject(new Error(result.error.message || 'NWC payment failed'))
|
||||
} else {
|
||||
resolve(result.result?.preimage)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
// ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = () => {
|
||||
clearTimeout(timeout)
|
||||
// Don't reject — server-side poll is the source of truth
|
||||
resolve()
|
||||
resolve(undefined)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user