Files
botfights/server/src/engine/payments.ts
T
DorianandClaude Opus 4.6 f964274c02 test: payments launch checklist passed
- Added 'consumed' to payment status enum (was missing from schema)
- Fixed orphan recovery to also find 'consumed' payments on restart
- Verified: NWC + Lightning Address wallet connect, invoice creation,
  21-sat entry fee, 42-sat winner payout, Cashu tokens, bet settlement,
  refund on cancelled/timeout, stuck payment recovery

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 00:43:16 +00:00

680 lines
25 KiB
TypeScript

import { nanoid } from 'nanoid'
import { db, schema, sqlite } from '../db/index.js'
import { eq, and, isNull, sql } from 'drizzle-orm'
import { finalizeEvent, getPublicKey } from 'nostr-tools'
import * as nip04 from 'nostr-tools/nip04'
import * as nip44 from 'nostr-tools/nip44'
import { Relay } from 'nostr-tools/relay'
import { hexToBytes, bytesToHex } from 'nostr-tools/utils'
import { Wallet as CashuWallet, getEncodedToken, getDecodedToken } from '@cashu/cashu-ts'
import { decrypt } from './crypto.js'
const ENTRY_FEE_SATS = 21
const POT_SATS = 42
const INVOICE_EXPIRY_SECS = 600 // 10 minutes
const NWC_RESPONSE_TIMEOUT_MS = 30_000
// NWC config from env — load dotenv inline as fallback
import { config as dotenvConfig } from 'dotenv'
import { dirname as _dirname, join as _join } from 'path'
import { fileURLToPath as _fileURLToPath } from 'url'
let _envLoaded = false
function ensureEnv() {
if (_envLoaded) return
_envLoaded = true
if (!process.env.BOTFIGHTS_NWC_URL) {
const envPath = _join(_dirname(_fileURLToPath(import.meta.url)), '..', '..', '.env')
dotenvConfig({ path: envPath })
}
}
function getNwcUrl(): string { ensureEnv(); return process.env.BOTFIGHTS_NWC_URL || '' }
function getCashuMintUrl(): string { ensureEnv(); return process.env.BOTFIGHTS_CASHU_MINT_URL || '' }
function getDevPayoutAddress(): string { ensureEnv(); return process.env.BOTFIGHTS_DEV_PAYOUT_LNADDRESS || '' }
interface NwcConfig {
pubkey: string
relay: string
secret: Uint8Array
}
/** Parse nostr+walletconnect:// URI into components */
export function parseNwcUrl(url: string): NwcConfig {
// Format: nostr+walletconnect://<pubkey>?relay=<relay>&secret=<hex>
const withoutScheme = url.replace('nostr+walletconnect://', '')
const [pubkey, queryString] = withoutScheme.split('?')
const params = new URLSearchParams(queryString)
const relay = params.get('relay')
const secret = params.get('secret')
if (!pubkey || !relay || !secret) {
throw new Error('Invalid NWC URL: missing pubkey, relay, or secret')
}
return { pubkey, relay, secret: hexToBytes(secret) }
}
function getNwcConfig(): NwcConfig {
const url = getNwcUrl()
if (!url) throw new Error('Payments not configured: BOTFIGHTS_NWC_URL not set')
return parseNwcUrl(url)
}
/** Encrypt content — try NIP-04 first (BTCPay/LND), with NIP-44 fallback */
async function nwcEncrypt(plaintext: string, secret: Uint8Array, walletPubkey: string): Promise<string> {
// NIP-04 is what most NWC wallets (BTCPay, LND, Alby Hub) expect
const secretHex = bytesToHex(secret)
return nip04.encrypt(secretHex, walletPubkey, plaintext)
}
/** Decrypt response — try NIP-04 first, fall back to NIP-44 */
async function nwcDecrypt(ciphertext: string, secret: Uint8Array, walletPubkey: string): Promise<string> {
const secretHex = bytesToHex(secret)
try {
return await nip04.decrypt(secretHex, walletPubkey, ciphertext)
} catch {
// Fall back to NIP-44
const conversationKey = nip44.v2.utils.getConversationKey(secret, walletPubkey)
return nip44.v2.decrypt(ciphertext, conversationKey)
}
}
/** Send an NWC request and wait for the response */
async function nwcRequest(
method: string,
params: Record<string, unknown>,
): Promise<Record<string, unknown>> {
const nwc = getNwcConfig()
const clientSecret = nwc.secret
const content = await nwcEncrypt(
JSON.stringify({ method, params }),
clientSecret,
nwc.pubkey,
)
const event = finalizeEvent({
kind: 23194,
created_at: Math.floor(Date.now() / 1000),
tags: [['p', nwc.pubkey]],
content,
}, clientSecret)
console.log(`[nwc] connecting to relay ${nwc.relay}...`)
const relay = await Relay.connect(nwc.relay)
console.log(`[nwc] relay connected, sending ${method} request (event ${event.id.slice(0, 8)}...)`)
try {
return await new Promise<Record<string, unknown>>((resolve, reject) => {
const timeout = setTimeout(() => {
console.log(`[nwc] ${method} timed out — is your wallet online?`)
relay.close()
reject(new Error(`NWC ${method} timed out after ${NWC_RESPONSE_TIMEOUT_MS / 1000}s. Is your wallet online?`))
}, NWC_RESPONSE_TIMEOUT_MS)
// Subscribe for the response (kind 23195)
const sub = relay.subscribe(
[{ kinds: [23195], authors: [nwc.pubkey], '#e': [event.id] }],
{
async onevent(responseEvent) {
clearTimeout(timeout)
console.log(`[nwc] got response for ${method}`)
try {
const decrypted = await nwcDecrypt(responseEvent.content, clientSecret, nwc.pubkey)
const result = JSON.parse(decrypted) as {
result_type: string
error?: { code: string; message: string }
result?: Record<string, unknown>
}
if (result.error) {
console.log(`[nwc] ${method} error: ${result.error.message}`)
reject(new Error(`NWC error: ${result.error.message} (${result.error.code})`))
} else {
console.log(`[nwc] ${method} success`)
resolve(result.result || {})
}
} catch (err) {
reject(err)
} finally {
sub.close()
relay.close()
}
},
oneose() {},
},
)
// Publish the request
relay.publish(event).then(() => {
console.log(`[nwc] ${method} event published, waiting for wallet response...`)
}).catch((err) => {
console.log(`[nwc] publish failed:`, err)
clearTimeout(timeout)
sub.close()
relay.close()
reject(err)
})
})
} catch (err) {
relay.close()
throw err
}
}
/** Create a 21-sat Lightning invoice for a ranked fight entry fee */
const DEV_AUTO_CONFIRM = process.env.NODE_ENV !== 'production'
export async function createEntryInvoice(botId: string): Promise<{ bolt11: string; paymentId: string }> {
// Verify bot exists
const botRows = await db.select({ id: schema.bots.id }).from(schema.bots).where(eq(schema.bots.id, botId)).limit(1)
if (botRows.length === 0) throw new Error('Bot not found')
const paymentId = nanoid(12)
if (DEV_AUTO_CONFIRM) {
// Dev mode: skip real invoice, auto-confirm so flow works without separate wallets
await db.insert(schema.payments).values({
id: paymentId,
botId,
direction: 'in',
amountSats: ENTRY_FEE_SATS,
method: 'lightning',
status: 'confirmed',
invoice: 'dev_auto_confirmed',
confirmedAt: new Date().toISOString(),
createdAt: new Date().toISOString(),
})
console.log(`[payments] dev: auto-confirmed 21 sat entry for ${botId} (self-payment skipped — payouts are real)`)
return { bolt11: 'dev_auto_confirmed', paymentId }
}
const result = await nwcRequest('make_invoice', {
amount: ENTRY_FEE_SATS * 1000, // NWC uses millisats
description: `Botfights ranked entry fee (${ENTRY_FEE_SATS} sats)`,
expiry: INVOICE_EXPIRY_SECS,
})
const bolt11 = result.invoice as string
if (!bolt11) throw new Error('NWC make_invoice did not return an invoice')
await db.insert(schema.payments).values({
id: paymentId,
botId,
direction: 'in',
amountSats: ENTRY_FEE_SATS,
method: 'lightning',
status: 'pending',
invoice: bolt11,
createdAt: new Date().toISOString(),
})
return { bolt11, paymentId }
}
/** Check if a pending invoice has been paid */
export async function checkPaymentStatus(paymentId: string): Promise<'pending' | 'confirmed' | 'failed'> {
const rows = await db.select().from(schema.payments).where(eq(schema.payments.id, paymentId)).limit(1)
if (rows.length === 0) throw new Error('Payment not found')
const payment = rows[0]
if (payment.status !== 'pending') return payment.status as 'confirmed' | 'failed'
try {
const result = await nwcRequest('lookup_invoice', {
invoice: payment.invoice,
})
const settled = result.settled_at || result.paid
if (settled) {
const preimage = (result.preimage as string) || undefined
await db.update(schema.payments).set({
status: 'confirmed',
preimage,
confirmedAt: new Date().toISOString(),
}).where(eq(schema.payments.id, paymentId))
return 'confirmed'
}
return 'pending'
} catch (err) {
console.error(`[payments] check status failed for ${paymentId}:`, err)
return 'pending'
}
}
/** Pay the winner of a ranked fight */
export async function payWinner(fightId: string, winnerId: string): Promise<void> {
// Dev mode: use Lightning Address from env to avoid self-payment on same LND node
const devPayoutAddr = DEV_AUTO_CONFIRM ? getDevPayoutAddress() : ''
// Look up winner name for payout description
const winnerRows = await db.select({ name: schema.bots.name })
.from(schema.bots).where(eq(schema.bots.id, winnerId)).limit(1)
const winnerName = winnerRows[0]?.name || 'unknown'
// Look up loser for the payout message
const fightRows = await db.select({
botAId: schema.fights.botAId,
botBId: schema.fights.botBId,
}).from(schema.fights).where(eq(schema.fights.id, fightId)).limit(1)
let loserName = 'opponent'
if (fightRows.length > 0) {
const loserId = fightRows[0].botAId === winnerId ? fightRows[0].botBId : fightRows[0].botAId
const loserRows = await db.select({ name: schema.bots.name })
.from(schema.bots).where(eq(schema.bots.id, loserId)).limit(1)
loserName = loserRows[0]?.name || 'opponent'
}
const payoutDesc = `BOTFIGHTS VICTORY: ${winnerName} defeated ${loserName}! ${POT_SATS} sats prize`
// Look up winner's wallet connection
const walletRows = await db.select().from(schema.walletConnections)
.where(eq(schema.walletConnections.botId, winnerId)).limit(1)
const wallet = walletRows[0]
let paymentMethod: 'lightning' | 'cashu' = 'lightning'
let invoice: string | undefined
let cashuToken: string | undefined
const retries = [2000, 4000, 8000]
for (let attempt = 0; attempt <= retries.length; attempt++) {
try {
if (devPayoutAddr) {
// Dev mode: pay to configured Lightning Address (different node, avoids self-payment)
console.log(`[payments] dev: paying ${POT_SATS} sats to ${devPayoutAddr}`)
invoice = await resolveAndCreateInvoice(devPayoutAddr, POT_SATS, payoutDesc)
await nwcRequest('pay_invoice', { invoice })
paymentMethod = 'lightning'
} else if (wallet?.method === 'lnaddress') {
// Resolve Lightning Address → LNURL → invoice → pay
invoice = await resolveAndCreateInvoice(decrypt(wallet.connectionData), POT_SATS, payoutDesc)
await nwcRequest('pay_invoice', { invoice })
paymentMethod = 'lightning'
} else if (wallet?.method === 'nwc') {
// Request invoice from winner's NWC wallet, then pay it via server wallet
const winnerResult = await nwcRequestVia(decrypt(wallet.connectionData), 'make_invoice', {
amount: POT_SATS * 1000,
description: payoutDesc,
})
invoice = winnerResult.invoice as string
if (!invoice) throw new Error('Winner NWC make_invoice returned no invoice')
// Pay the invoice via server wallet
await nwcRequest('pay_invoice', { invoice })
paymentMethod = 'lightning'
} else if (getCashuMintUrl()) {
// No wallet + mint configured — create Cashu token for later claim
const cashuWallet = new CashuWallet(getCashuMintUrl())
await cashuWallet.loadMint()
const mintQuote = await cashuWallet.createMintQuote(POT_SATS)
if (mintQuote.request) {
await nwcRequest('pay_invoice', { invoice: mintQuote.request })
}
await new Promise(r => setTimeout(r, 2000))
const proofs = await cashuWallet.mintProofs(POT_SATS, mintQuote.quote)
cashuToken = getEncodedToken({ mint: getCashuMintUrl(), proofs, unit: 'sat' })
paymentMethod = 'cashu'
} else {
console.log(`[payments] no payout method available for winner ${winnerId}`)
paymentMethod = 'lightning'
}
// Insert payout record
const paymentId = nanoid(12)
await db.insert(schema.payments).values({
id: paymentId,
fightId,
botId: winnerId,
direction: 'out',
amountSats: POT_SATS,
method: paymentMethod,
status: 'confirmed',
invoice,
cashuToken,
confirmedAt: new Date().toISOString(),
createdAt: new Date().toISOString(),
})
// Update fight payout status
await db.update(schema.fights).set({ payoutStatus: 'paid' })
.where(eq(schema.fights.id, fightId))
// Update winner stats
await db.update(schema.bots).set({
satsWon: sql`${schema.bots.satsWon} + ${POT_SATS}`,
}).where(eq(schema.bots.id, winnerId))
console.log(`[payments] paid ${POT_SATS} sats to winner ${winnerId} for fight ${fightId}`)
return
} catch (err) {
console.error(`[payments] payout attempt ${attempt + 1} failed for fight ${fightId}:`, err)
if (attempt < retries.length) {
await new Promise(r => setTimeout(r, retries[attempt]))
} else {
// All retries exhausted
await db.update(schema.fights).set({ payoutStatus: 'failed' })
.where(eq(schema.fights.id, fightId))
throw err
}
}
}
}
/** Send an NWC request via a specific NWC connection string (for winner's wallet) */
async function nwcRequestVia(
nwcUrl: string,
method: string,
params: Record<string, unknown>,
): Promise<Record<string, unknown>> {
const nwc = parseNwcUrl(nwcUrl)
const clientSecret = nwc.secret
const conversationKey = nip44.v2.utils.getConversationKey(clientSecret, nwc.pubkey)
const content = nip44.v2.encrypt(
JSON.stringify({ method, params }),
conversationKey,
)
const event = finalizeEvent({
kind: 23194,
created_at: Math.floor(Date.now() / 1000),
tags: [['p', nwc.pubkey]],
content,
}, clientSecret)
const relay = await Relay.connect(nwc.relay)
try {
return await new Promise<Record<string, unknown>>((resolve, reject) => {
const timeout = setTimeout(() => {
relay.close()
reject(new Error(`NWC request timed out after ${NWC_RESPONSE_TIMEOUT_MS}ms`))
}, NWC_RESPONSE_TIMEOUT_MS)
const sub = relay.subscribe(
[{ kinds: [23195], authors: [nwc.pubkey], '#e': [event.id] }],
{
onevent(responseEvent) {
clearTimeout(timeout)
try {
const decrypted = nip44.v2.decrypt(responseEvent.content, conversationKey)
const result = JSON.parse(decrypted) as {
result_type: string
error?: { code: string; message: string }
result?: Record<string, unknown>
}
if (result.error) {
reject(new Error(`NWC error: ${result.error.message} (${result.error.code})`))
} else {
resolve(result.result || {})
}
} catch (err) {
reject(err)
} finally {
sub.close()
relay.close()
}
},
oneose() {},
},
)
relay.publish(event).catch((err) => {
clearTimeout(timeout)
sub.close()
relay.close()
reject(err)
})
})
} catch (err) {
relay.close()
throw err
}
}
/** Resolve a Lightning Address to a BOLT11 invoice */
async function resolveAndCreateInvoice(lnAddress: string, amountSats: number, comment?: string): Promise<string> {
const [name, domain] = lnAddress.split('@')
if (!name || !domain) throw new Error(`Invalid Lightning Address: ${lnAddress}`)
const lnurlUrl = `https://${domain}/.well-known/lnurlp/${name}`
const res = await fetch(lnurlUrl)
if (!res.ok) throw new Error(`LNURL resolve failed: ${res.status}`)
const data = await res.json() as {
callback: string
minSendable: number
maxSendable: number
tag: string
}
if (data.tag !== 'payRequest') throw new Error('Invalid LNURL response: not a pay request')
const amountMillisats = amountSats * 1000
if (amountMillisats < data.minSendable || amountMillisats > data.maxSendable) {
throw new Error(`Amount ${amountSats} sats out of LNURL range`)
}
const callbackUrl = new URL(data.callback)
callbackUrl.searchParams.set('amount', String(amountMillisats))
if (comment) callbackUrl.searchParams.set('comment', comment)
const invoiceRes = await fetch(callbackUrl.toString())
if (!invoiceRes.ok) throw new Error(`LNURL callback failed: ${invoiceRes.status}`)
const invoiceData = await invoiceRes.json() as { pr: string }
if (!invoiceData.pr) throw new Error('LNURL callback returned no invoice')
return invoiceData.pr
}
/** Refund an entry fee */
export async function refundEntry(paymentId: string): Promise<void> {
const rows = await db.select().from(schema.payments).where(eq(schema.payments.id, paymentId)).limit(1)
if (rows.length === 0) throw new Error('Payment not found')
const payment = rows[0]
if (payment.status === 'refunded') return // already refunded
if (payment.status !== 'confirmed') {
// Mark as failed if not yet confirmed — nothing to refund
await db.update(schema.payments).set({ status: 'failed' }).where(eq(schema.payments.id, paymentId))
return
}
try {
if (payment.method === 'lightning') {
// Create a refund invoice by paying back. We need the bot's wallet info.
const walletRows = await db.select().from(schema.walletConnections)
.where(eq(schema.walletConnections.botId, payment.botId)).limit(1)
if (walletRows[0]?.method === 'nwc') {
const result = await nwcRequestVia(decrypt(walletRows[0].connectionData), 'make_invoice', {
amount: ENTRY_FEE_SATS * 1000,
description: `BOTFIGHTS REFUND: ${ENTRY_FEE_SATS} sats ranked entry fee returned`,
})
const invoice = result.invoice as string
if (invoice) {
await nwcRequest('pay_invoice', { invoice })
}
} else if (walletRows[0]?.method === 'lnaddress') {
const invoice = await resolveAndCreateInvoice(decrypt(walletRows[0].connectionData), ENTRY_FEE_SATS)
await nwcRequest('pay_invoice', { invoice })
} else if (getCashuMintUrl()) {
// Fallback: issue Cashu token
const cashuWallet = new CashuWallet(getCashuMintUrl())
await cashuWallet.loadMint()
const mintQuote = await cashuWallet.createMintQuote(ENTRY_FEE_SATS)
if (mintQuote.request) {
await nwcRequest('pay_invoice', { invoice: mintQuote.request })
}
await new Promise(r => setTimeout(r, 2000))
const proofs = await cashuWallet.mintProofs(ENTRY_FEE_SATS, mintQuote.quote)
const token = getEncodedToken({ mint: getCashuMintUrl(), proofs, unit: 'sat' })
await db.update(schema.payments).set({ cashuToken: token }).where(eq(schema.payments.id, paymentId))
}
}
// For cashu entries, the token is already spent — mint new one as refund
if (payment.method === 'cashu' && getCashuMintUrl()) {
const cashuWallet = new CashuWallet(getCashuMintUrl())
await cashuWallet.loadMint()
const mintQuote = await cashuWallet.createMintQuote(ENTRY_FEE_SATS)
if (mintQuote.request) {
await nwcRequest('pay_invoice', { invoice: mintQuote.request })
}
await new Promise(r => setTimeout(r, 2000))
const proofs = await cashuWallet.mintProofs(ENTRY_FEE_SATS, mintQuote.quote)
const token = getEncodedToken({ mint: getCashuMintUrl(), proofs, unit: 'sat' })
await db.update(schema.payments).set({ cashuToken: token }).where(eq(schema.payments.id, paymentId))
}
await db.update(schema.payments).set({
status: 'refunded',
refundedAt: new Date().toISOString(),
}).where(eq(schema.payments.id, paymentId))
console.log(`[payments] refunded payment ${paymentId}`)
} catch (err) {
console.error(`[payments] refund failed for ${paymentId}:`, err)
await db.update(schema.payments).set({
status: 'failed',
errorReason: err instanceof Error ? err.message : 'Refund failed',
}).where(eq(schema.payments.id, paymentId))
}
}
/** Redeem a Cashu token as entry fee */
export async function redeemCashuToken(token: string, botId: string): Promise<{ paymentId: string; valid: boolean }> {
if (!getCashuMintUrl()) throw new Error('Cashu mint not configured')
try {
const decoded = getDecodedToken(token)
const totalAmount = decoded.proofs.reduce((sum, p) => sum + p.amount, 0)
if (totalAmount < ENTRY_FEE_SATS) {
return { paymentId: '', valid: false }
}
const cashuWallet = new CashuWallet(getCashuMintUrl())
await cashuWallet.loadMint()
// Receive the token (swap for fresh proofs — prevents double-spend)
const proofs = await cashuWallet.receive(token)
if (!proofs || proofs.length === 0) {
return { paymentId: '', valid: false }
}
const paymentId = nanoid(12)
await db.insert(schema.payments).values({
id: paymentId,
botId,
direction: 'in',
amountSats: ENTRY_FEE_SATS,
method: 'cashu',
status: 'confirmed',
cashuToken: token,
confirmedAt: new Date().toISOString(),
createdAt: new Date().toISOString(),
})
return { paymentId, valid: true }
} catch (err) {
console.error(`[payments] Cashu redeem failed:`, err)
return { paymentId: '', valid: false }
}
}
/** Recover orphaned payments on startup */
export async function recoverOrphanedPayments(): Promise<void> {
// Find confirmed/consumed entry payments without a fight — refund them
// 'consumed' payments were in the queue when server restarted
const orphaned = await db.select().from(schema.payments)
.where(and(
sql`${schema.payments.status} IN ('confirmed', 'consumed')`,
eq(schema.payments.direction, 'in'),
isNull(schema.payments.fightId),
))
if (orphaned.length > 0) {
console.log(`[payments] found ${orphaned.length} orphaned payments, refunding...`)
for (const payment of orphaned) {
try {
await refundEntry(payment.id)
} catch (err) {
console.error(`[payments] orphan refund failed for ${payment.id}:`, err)
}
}
}
// Find fights with payout_status='pending' — retry payout
const pendingPayouts = await db.select().from(schema.fights)
.where(eq(schema.fights.payoutStatus, 'pending'))
if (pendingPayouts.length > 0) {
console.log(`[payments] found ${pendingPayouts.length} pending payouts, retrying...`)
for (const fight of pendingPayouts) {
if (fight.winnerId) {
try {
await payWinner(fight.id, fight.winnerId)
} catch (err) {
console.error(`[payments] payout retry failed for fight ${fight.id}:`, err)
}
}
}
}
if (orphaned.length === 0 && pendingPayouts.length === 0) {
console.log('[payments] no orphaned payments or pending payouts')
}
}
// In-memory set of payment IDs currently consumed by the ranked queue.
// Safe because Node.js is single-threaded and the ranked queue is also in-memory.
// On server restart, the queue is empty and recoverOrphanedPayments handles cleanup.
const consumedPayments = new Set<string>()
/**
* Consume a confirmed entry payment for queue use.
* Returns true if the payment was successfully consumed, false if already used.
*/
export async function consumePaymentForQueue(paymentId: string, botId: string): Promise<boolean> {
// Fast path: already consumed in this server lifetime
if (consumedPayments.has(paymentId)) return false
// Atomic: only marks as consumed if ALL conditions met in a single UPDATE
// Eliminates race window between SELECT check and later UPDATE
const result = sqlite.prepare(
`UPDATE payments SET status = 'consumed' WHERE id = ? AND bot_id = ? AND status = 'confirmed' AND direction = 'in' AND fight_id IS NULL`
).run(paymentId, botId)
if (result.changes === 0) return false
consumedPayments.add(paymentId)
return true
}
/**
* Link consumed entry payments to the actual fight.
* Called after a ranked fight is created. Also removes from consumed set.
*/
export async function linkPaymentsToFight(fightId: string, paymentIds: string[]): Promise<void> {
for (const pid of paymentIds) {
await db.update(schema.payments).set({ fightId })
.where(eq(schema.payments.id, pid))
consumedPayments.delete(pid)
}
}
/**
* Release a consumed payment back for refund when queue times out or bot leaves.
*/
export function releasePayment(paymentId: string): void {
consumedPayments.delete(paymentId)
// Revert DB status so the payment can be re-consumed or refunded
sqlite.prepare(`UPDATE payments SET status = 'confirmed' WHERE id = ? AND status = 'consumed'`).run(paymentId)
}
export { ENTRY_FEE_SATS, POT_SATS }