import config from "$config"; import { db, g, s } from "$lib/db"; import { request } from "$lib/ecash"; import { deriveAddress } from "$lib/esplora"; import ln from "$lib/ln"; import { emit } from "$lib/sockets"; import { SATS, bip21, fail, getInvoice, getUser } from "$lib/utils"; import rpc from "@coinos/rpc"; import { v4 } from "uuid"; import { PaymentType } from "$lib/types"; const bc = rpc(config.bitcoin); const lq = rpc(config.liquid); export const generate = async ({ invoice, user }) => { let { address_type, bolt11, bolt12, aid, currency, expiry, fiat, id, hash, tip, amount, items = [], own, rate, request_id, memo, memoPrompt, prompt, type = PaymentType.lightning, webhook, secret, forward, } = invoice; amount = Number.parseInt(amount || 0); tip = tip == null ? null : Number.parseInt(tip); if (user) user = await getUser(user.username); if (!user) fail("user not provided"); if (typeof prompt === "undefined") prompt = user.prompt; let account = await g(`account:${aid}`); if (!account) account = await g(`account:${user.id}`); if (!account) fail("account not found"); aid = account.id; const rates = await g("rates"); if (!currency) { const acct = aid ? await g(`account:${aid}`) : null; currency = acct?.currency || user.currency; } if (!rate) rate = rates[currency]; if (fiat) amount = Math.round((SATS * fiat) / rate); if (amount < 0) fail("invalid amount"); if (tip < 0) fail("invalid tip"); if (rate < 0) fail("invalid rate"); if (memo && memo.length > 5000) fail("memo too long"); if (!id) id = v4(); let text; let path; let paymentHash; if (type === PaymentType.ark && account.type !== "ark") { // Custodial account receiving via Ark: use vault's ark address const accounts = await db.lRange(`${user.id}:accounts`, 0, -1); let vaultAddress; for (const accId of accounts) { const acc = await g(`account:${accId}`); if (acc?.type === "ark" && acc.arkAddress) { vaultAddress = acc.arkAddress; break; } } if (!vaultAddress) fail("No ark vault found"); text = vaultAddress; hash = id; await s(`custodial-ark-invoice:${vaultAddress}`, id); } else if (account.type === "ark") { type = PaymentType.ark; text = account.arkAddress; hash = id; await db.del(`custodial-ark-invoice:${account.arkAddress}`); } else if (account.seed || (account.pubkey && account.fingerprint)) { type = PaymentType.bitcoin; const nextIndex = account.nextIndex || 0; const { address, path: hdpath } = deriveAddress(account.pubkey, account.fingerprint, nextIndex); hash = address; path = hdpath; text = bip21(hash, invoice); account.nextIndex = nextIndex + 1; await s(`account:${aid}`, account); await s(`invoice:${hash}`, id); await db.sAdd("watching", hash); } else if (type === PaymentType.lightning) { let r; if (bolt11) { const { id: nodeid } = await ln.getinfo(); r = await ln.decode(bolt11); if (r.payee !== nodeid) fail("invalid invoice"); amount = Math.round(r.amount_msat / 1000); r.bolt11 = bolt11; } else { expiry ||= 60 * 60 * 24 * 30; r = await ln.invoice({ amount_msat: amount ? `${amount + tip}sat` : "any", label: `${id} ${user.username} ${Date.now()}`, description: memo || "", expiry, deschashonly: true, cltv: 19, }); } hash = r.bolt11; text = r.bolt11; paymentHash = r.payment_hash; } else if (type === PaymentType.bolt12) { let r; if (bolt12) { const { id: nodeid } = await ln.getinfo(); r = await ln.decode(bolt12); if (r.invoice_node_id !== nodeid) fail("invalid invoice"); amount = Math.round(r.invoice_amount_msat / 1000); r.bolt12 = bolt12; } else { r = await ln.offer({ amount: amount ? `${amount + tip}sat` : "any", label: `${id} ${user.username} ${new Date()}`, description: memo || id, }); if (await getInvoice(r.offer_id)) fail("Duplicate offer exists"); await s(`invoice:${r.offer_id}`, id); } hash = r.bolt12; text = r.bolt12; } else if (type === PaymentType.bitcoin) { address_type ||= "bech32"; hash = await bc.getNewAddress({ address_type }); text = bip21(hash, invoice); await db.sAdd("watching", hash); } else if (type === PaymentType.liquid) { address_type ||= "blech32"; hash = await lq.getNewAddress({ address_type }); text = bip21(hash, invoice); } else if (type === PaymentType.internal) { hash = id; } else if (type === PaymentType.ecash) { hash = id; text = request(id, amount, memo); } else if (type === PaymentType.ark) { hash = id; } else { fail(`unrecognized type ${type}`); } invoice = { amount, aid, address_type, created: Date.now(), currency, hash, expiry, id, items, memo, path, rate, paymentHash, pending: 0, received: 0, request_id, memoPrompt, own, prompt, secret, text, tip, type, uid: user.id, webhook, forward, }; if (type === "liquid") { const { unconfidential } = await lq.getAddressInfo(hash); await s(`invoice:${unconfidential}`, id); } await s(`invoice:${hash}`, id); await s(`invoice:${id}`, invoice); await db.lPush(`${aid}:invoices`, id); if (request_id) { const request = await g(`request:${request_id}`); if (request) { const { invoice_id: prev } = request; request.invoice_id = id; await s(`request:${request_id}`, request); if (!prev) emit(request.requester_id, "invoice", invoice); } } return invoice; };