import store from "./store"; import bolt11 from "bolt11"; import { Op } from "@sequelize/core"; import { computeConversionFee, conversionFeeReceiver } from "../routes/lightning/conversionFee"; import ln from "$lib/ln"; export const send = async (amount, memo, hash, user) => { l("attempting lightning payment", user.username, amount, hash); let payreq = bolt11.decode(hash); let routingInfo = payreq.tags.find(t => t.tagName === "routing_info"); if ( routingInfo && routingInfo.data.find(d => d.fee_proportional_millionths > 10000) ) { warn("fee rate too high", d.fee_proportional_millionths); throw new Error( "Couldn't find a suitable route to pay to your destination" ); } if (!amount || payreq.satoshis > amount) amount = payreq.satoshis; if (seen.includes(hash)) { warn("attempted to pay a paid invoice", user.username); throw new Error("Invoice has been paid, can't pay again"); } let error, fee, total, preimage, payment; let conversionFee = computeConversionFee(amount); let conversionFeeDeduction; await db.transaction(async transaction => { l("starting lightning send transaction"); let account; if (user.account.asset === config.liquid.btcasset) account = await db.Account.findOne({ where: { id: user.account.id, pubkey: null }, lock: transaction.LOCK.UPDATE, transaction }); else account = await db.Account.findOne({ where: { user_id: user.id, asset: config.liquid.btcasset, pubkey: null, balance: { [Op.gte]: amount } }, lock: transaction.LOCK.UPDATE, transaction }); conversionFeeDeduction = Math.min(account.lightning_credits, conversionFee); if ( !account || account.balance < amount + conversionFee - conversionFeeDeduction ) { warn( "user attempting to send more than they have", user.id, amount, conversionFee ); throw new Error("Insufficient funds"); } await account.decrement( { balance: amount, lightning_credits: conversionFeeDeduction }, { transaction } ); await account.reload({ transaction }); l("debited account", account.id); if (config.lna.clightning) { let m = await ln.pay(hash); let sent; ({ msatoshi: total, msatoshi_sent: sent, payment_preimage: preimage } = m); fee = parseInt((sent - total) / 1000); total = parseInt(total / 1000); } else { let m = await ln.pay({ tokens: amount, request: hash, max_paths: 10, max_fee: { fixed: Math.min(10000, account.balance) } }); if (!m) throw new Error(`lightning payment failed ${amount} ${hash}`); if (m.payment_error) throw new Error(m.payment_error); ({ fee, secret: preimage, tokens: total } = m); } preimage = preimage.toString("hex"); if (seen.includes(preimage)) { warn("duplicate payment detected", preimage); throw new Error("Duplicate payment detected"); } }); await db.transaction(async transaction => { l("starting lightning fee transaction"); let account; if (user.account.asset === config.liquid.btcasset) account = await db.Account.findOne({ where: { id: user.account.id, pubkey: null }, lock: transaction.LOCK.UPDATE, transaction }); else account = await db.Account.findOne({ where: { user_id: user.id, asset: config.liquid.btcasset, pubkey: null, balance: { [Op.gte]: fee } }, lock: transaction.LOCK.UPDATE, transaction }); total = parseInt(total); fee = parseInt(fee); await account.decrement({ balance: fee }, { transaction }); await account.reload({ transaction }); if (conversionFeeDeduction) { conversionFee -= conversionFeeDeduction; } let receiverAccount = await db.Account.findOne({ where: { "$user.username$": conversionFeeReceiver }, include: [ { model: db.User, as: "user" } ], lock: transaction.LOCK.UPDATE, transaction }); let fee_payment; let fee_payment_id = null; if (conversionFee) { await receiverAccount.increment( { balance: conversionFee }, { transaction } ); await receiverAccount.reload({ transaction }); fee_payment = await db.Payment.create( { amount: conversionFee, fee: 0, memo: "Lightning conversion fee", account_id: receiverAccount.id, user_id: receiverAccount.user_id, rate: store.rates[receiverAccount.user.currency], currency: receiverAccount.user.currency, confirmed: true, received: true, network: "COINOS" }, { transaction } ); fee_payment_id = fee_payment.id; } payment = await db.Payment.create( { amount: -(total - fee), account_id: account.id, user_id: user.id, hash, memo, preimage, rate: store.rates[user.currency], currency: user.currency, confirmed: true, network: "lightning", fee, fee_payment_id }, { transaction } ); l("created payment record", payment.id); payment = payment.get({ plain: true }); payment.account = account.get({ plain: true }); payment.fee_payment = fee_payment && fee_payment.get({ plain: true }); emit(user.username, "account", payment.account); emit(user.username, "payment", payment); seen.push(preimage); seen.push(hash); l("sent lightning", user.username, total - fee, fee); }); return payment; }; weight: bold } /* Keyword.Declaration */ .highlight .kn { color: #080; font-weight: bold } /* Keyword.Namespace */ .highlight .kp { color: #080 } /* Keyword.Pseudo */ .highlight .kr { color: #080; font-weight: bold } /* Keyword.Reserved */ .highlight .kt { color: #888; font-weight: bold } /* Keyword.Type */ .highlight .m { color: #00D; font-weight: bold } /* Literal.Number */ .highlight .s { color: #D20; background-color: #FFF0F0 } /* Literal.String */ .highlight .na { color: #369 } /* Name.Attribute */ .highlight .nb { color: #038 } /* Name.Builtin */ .highlight .nc { color: #B06; font-weight: bold } /* Name.Class */ .highlight .no { color: #036; font-weight: bold } /* Name.Constant */ .highlight .nd { color: #555 } /* Name.Decorator */ .highlight .ne { color: #B06; font-weight: bold } /* Name.Exception */ .highlight .nf { color: #06B; font-weight: bold } /* Name.Function */ .highlight .nl { color: #369; font-style: italic } /* Name.Label */ .highlight .nn { color: #B06; font-weight: bold } /* Name.Namespace */ .highlight .py { color: #369; font-weight: bold } /* Name.Property */ .highlight .nt { color: #B06; font-weight: bold } /* Name.Tag */ .highlight .nv { color: #369 } /* Name.Variable */ .highlight .ow { color: #080 } /* Operator.Word */ .highlight .w { color: #BBB } /* Text.Whitespace */ .highlight .mb { color: #00D; font-weight: bold } /* Literal.Number.Bin */ .highlight .mf { color: #00D; font-weight: bold } /* Literal.Number.Float */ .highlight .mh { color: #00D; font-weight: bold } /* Literal.Number.Hex */ .highlight .mi { color: #00D; font-weight: bold } /* Literal.Number.Integer */ .highlight .mo { color: #00D; font-weight: bold } /* Literal.Number.Oct */ .highlight .sa { color: #D20; background-color: #FFF0F0 } /* Literal.String.Affix */ .highlight .sb { color: #D20; background-color: #FFF0F0 } /* Literal.String.Backtick */ .highlight .sc { color: #D20; background-color: #FFF0F0 } /* Literal.String.Char */ .highlight .dl { color: #D20; background-color: #FFF0F0 } /* Literal.String.Delimiter */ .highlight .sd { color: #D20; background-color: #FFF0F0 } /* Literal.String.Doc */ .highlight .s2 { color: #D20; background-color: #FFF0F0 } /* Literal.String.Double */ .highlight .se { color: #04D; background-color: #FFF0F0 } /* Literal.String.Escape */ .highlight .sh { color: #D20; background-color: #FFF0F0 } /* Literal.String.Heredoc */ .highlight .si { color: #33B; background-color: #FFF0F0 } /* Literal.String.Interpol */ .highlight .sx { color: #2B2; background-color: #F0FFF0 } /* Literal.String.Other */ .highlight .sr { color: #080; background-color: #FFF0FF } /* Literal.String.Regex */ .highlight .s1 { color: #D20; background-color: #FFF0F0 } /* Literal.String.Single */ .highlight .ss { color: #A60; background-color: #FFF0F0 } /* Literal.String.Symbol */ .highlight .bp { color: #038 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #06B; font-weight: bold } /* Name.Function.Magic */ .highlight .vc { color: #369 } /* Name.Variable.Class */ .highlight .vg { color: #D70 } /* Name.Variable.Global */ .highlight .vi { color: #33B } /* Name.Variable.Instance */ .highlight .vm { color: #369 } /* Name.Variable.Magic */ .highlight .il { color: #00D; font-weight: bold } /* Literal.Number.Integer.Long */
// Dev server: rebuilds the inlined bundle on every request so you just refresh
// the browser to see changes. Same output as `bun run build`, unminified.

import { buildHtml, buildJsQr } from './build.js';

const port = Number(process.env.PORT || 5173);

Bun.serve({
  port,
  async fetch(req) {
    try {
      const path = new URL(req.url).pathname;
      if (path === '/jsqr.js') {
        return new Response(await buildJsQr({ minify: false }), {
          headers: { 'content-type': 'text/javascript; charset=utf-8' },
        });
      }
      const loc = path.match(/^\/locales\/([a-z]{2})\.json$/);
      if (loc) {
        const f = Bun.file('src/locales/' + loc[1] + '.json');
        return (await f.exists())
          ? new Response(f, { headers: { 'content-type': 'application/json; charset=utf-8' } })
          : new Response('{}', { status: 404, headers: { 'content-type': 'application/json' } });
      }
      const html = await buildHtml({ minify: false });
      return new Response(html, {
        headers: { 'content-type': 'text/html; charset=utf-8' },
      });
    } catch (e) {
      return new Response(`Build error:\n\n${e.stack || e}`, {
        status: 500,
        headers: { 'content-type': 'text/plain; charset=utf-8' },
      });
    }
  },
});

console.log(`dev server → http://localhost:${port}`);