From 2bcd5e9cd2233934b6d78724a07172b187ceb0b0 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Mon, 3 Aug 2020 21:38:29 -0700 Subject: user urls --- config/rpc.proto | 170 +++++++++++++++++-- index.js | 3 +- lib/send.js | 2 +- lib/sockets.js | 26 ++- routes/invoices.js | 23 ++- routes/lightning/invoice.js | 2 - routes/lightning/receive.js | 161 +++++++++--------- routes/lnurl.js | 389 +++++++++++++++++++++++--------------------- routes/payments.js | 2 +- routes/users.js | 12 +- 10 files changed, 489 insertions(+), 301 deletions(-) diff --git a/config/rpc.proto b/config/rpc.proto index f69b768f..ee5504d7 100644 --- a/config/rpc.proto +++ b/config/rpc.proto @@ -140,6 +140,14 @@ service Lightning { */ rpc GetInfo (GetInfoRequest) returns (GetInfoResponse); + /** lncli: `getrecoveryinfo` + GetRecoveryInfo returns information concerning the recovery mode including + whether it's in a recovery mode, whether the recovery is finished, and the + progress made so far. + */ + rpc GetRecoveryInfo (GetRecoveryInfoRequest) + returns (GetRecoveryInfoResponse); + // TODO(roasbeef): merge with below with bool? /* lncli: `pendingchannels` PendingChannels returns a list of all the channels that are currently @@ -985,6 +993,30 @@ enum CommitmentType { UNKNOWN_COMMITMENT_TYPE = 999; } +message ChannelConstraints { + /* + The CSV delay expressed in relative blocks. If the channel is force closed, + we will need to wait for this many blocks before we can regain our funds. + */ + uint32 csv_delay = 1; + + // The minimum satoshis this node is required to reserve in its balance. + uint64 chan_reserve_sat = 2; + + // The dust limit (in satoshis) of the initiator's commitment tx. + uint64 dust_limit_sat = 3; + + // The maximum amount of coins in millisatoshis that can be pending in this + // channel. + uint64 max_pending_amt_msat = 4; + + // The smallest HTLC in millisatoshis that the initiator will accept. + uint64 min_htlc_msat = 5; + + // The total number of incoming HTLC's that the initiator will accept. + uint32 max_accepted_htlcs = 6; +} + message Channel { // Whether this channel is active or not bool active = 1; @@ -1057,10 +1089,11 @@ message Channel { repeated HTLC pending_htlcs = 15; /* - The CSV delay expressed in relative blocks. If the channel is force closed, - we will need to wait for this many blocks before we can regain our funds. + Deprecated. The CSV delay expressed in relative blocks. If the channel is + force closed, we will need to wait for this many blocks before we can regain + our funds. */ - uint32 csv_delay = 16; + uint32 csv_delay = 16 [deprecated = true]; // Whether this channel is advertised to the network or not. bool private = 17; @@ -1071,13 +1104,15 @@ message Channel { // A set of flags showing the current state of the channel. string chan_status_flags = 19; - // The minimum satoshis this node is required to reserve in its balance. - int64 local_chan_reserve_sat = 20; + // Deprecated. The minimum satoshis this node is required to reserve in its + // balance. + int64 local_chan_reserve_sat = 20 [deprecated = true]; /* - The minimum satoshis the other node is required to reserve in its balance. + Deprecated. The minimum satoshis the other node is required to reserve in + its balance. */ - int64 remote_chan_reserve_sat = 21; + int64 remote_chan_reserve_sat = 21 [deprecated = true]; // Deprecated. Use commitment_type. bool static_remote_key = 22 [deprecated = true]; @@ -1122,9 +1157,17 @@ message Channel { frozen channel doest not allow a cooperative channel close by the initiator. The thaw_height is the height that this restriction stops applying to the channel. This field is optional, not setting it or using a - value of zero will mean the channel has no additional restrictions. + value of zero will mean the channel has no additional restrictions. The + height can be interpreted in two ways: as a relative height if the value is + less than 500,000, or as an absolute height otherwise. */ uint32 thaw_height = 28; + + // List constraints for the local node. + ChannelConstraints local_constraints = 29; + + // List constraints for the remote node. + ChannelConstraints remote_constraints = 30; } message ListChannelsRequest { @@ -1206,6 +1249,79 @@ message ChannelCloseSummary { force closes, although only one party's close will be confirmed on chain. */ Initiator close_initiator = 12; + + repeated Resolution resolutions = 13; +} + +enum ResolutionType { + TYPE_UNKNOWN = 0; + + // We resolved an anchor output. + ANCHOR = 1; + + /* + We are resolving an incoming htlc on chain. This if this htlc is + claimed, we swept the incoming htlc with the preimage. If it is timed + out, our peer swept the timeout path. + */ + INCOMING_HTLC = 2; + + /* + We are resolving an outgoing htlc on chain. If this htlc is claimed, + the remote party swept the htlc with the preimage. If it is timed out, + we swept it with the timeout path. + */ + OUTGOING_HTLC = 3; + + // We force closed and need to sweep our time locked commitment output. + COMMIT = 4; +} + +enum ResolutionOutcome { + // Outcome unknown. + OUTCOME_UNKNOWN = 0; + + // An output was claimed on chain. + CLAIMED = 1; + + // An output was left unclaimed on chain. + UNCLAIMED = 2; + + /* + ResolverOutcomeAbandoned indicates that an output that we did not + claim on chain, for example an anchor that we did not sweep and a + third party claimed on chain, or a htlc that we could not decode + so left unclaimed. + */ + ABANDONED = 3; + + /* + If we force closed our channel, our htlcs need to be claimed in two + stages. This outcome represents the broadcast of a timeout or success + transaction for this two stage htlc claim. + */ + FIRST_STAGE = 4; + + // A htlc was timed out on chain. + TIMEOUT = 5; +} + +message Resolution { + // The type of output we are resolving. + ResolutionType resolution_type = 1; + + // The outcome of our on chain action that resolved the outpoint. + ResolutionOutcome outcome = 2; + + // The outpoint that was spent by the resolution. + OutPoint outpoint = 3; + + // The amount that was claimed by the resolution. + uint64 amount_sat = 4; + + // The hex-encoded transaction ID of the sweep transaction that spent the + // output. + string sweep_txid = 5; } message ClosedChannelsRequest { @@ -1381,6 +1497,19 @@ message GetInfoResponse { map features = 19; } +message GetRecoveryInfoRequest { +} +message GetRecoveryInfoResponse { + // Whether the wallet is in recovery mode + bool recovery_mode = 1; + + // Whether the wallet recovery progress is finished + bool recovery_finished = 2; + + // The recovery progress, ranging from 0 to 1. + double progress = 3; +} + message Chain { // The blockchain the node is on (eg bitcoin, litecoin) string chain = 1; @@ -1537,6 +1666,12 @@ message OpenChannelRequest { carried out in an interactive manner (PSBT based). */ FundingShim funding_shim = 14; + + /* + The maximum amount of coins in millisatoshi that can be pending within + the channel. It only applies to the remote party. + */ + uint64 remote_max_value_in_flight_msat = 15; } message OpenStatusUpdate { oneof update { @@ -1611,10 +1746,11 @@ message ChanPointShim { bytes pending_chan_id = 5; /* - This uint32 indicates if this channel is to be considered 'frozen'. A - frozen channel does not allow a cooperative channel close by the - initiator. The thaw_height is the height that this restriction stops - applying to the channel. + This uint32 indicates if this channel is to be considered 'frozen'. A frozen + channel does not allow a cooperative channel close by the initiator. The + thaw_height is the height that this restriction stops applying to the + channel. The height can be interpreted in two ways: as a relative height if + the value is less than 500,000, or as an absolute height otherwise. */ uint32 thaw_height = 6; } @@ -1632,6 +1768,16 @@ message PsbtShim { non-empty, it must be a binary serialized PSBT. */ bytes base_psbt = 2; + + /* + If a channel should be part of a batch (multiple channel openings in one + transaction), it can be dangerous if the whole batch transaction is + published too early before all channel opening negotiations are completed. + This flag prevents this particular channel from broadcasting the transaction + after the negotiation with the remote peer. In a batch of channel openings + this flag should be set to true for every channel but the very last. + */ + bool no_publish = 3; } message FundingShim { diff --git a/index.js b/index.js index 4ea48737..c03936f2 100644 --- a/index.js +++ b/index.js @@ -65,7 +65,8 @@ app.use((err, req, res, next) => { if (req.user) details.username = req.user.username; - l.error("uncaught error", details, err.message); + l.error("Error: ", err.message); + throw err; res.status(500); res.send(err.message); return res.end(); diff --git a/lib/send.js b/lib/send.js index 2e4f1f12..0fd76cc5 100644 --- a/lib/send.js +++ b/lib/send.js @@ -5,7 +5,7 @@ send = async (amount, memo, hash, user) => { if (!amount) amount = payreq.satoshis; - l.info("attempting lightning payment", user.username, amount); + l.info("attempting lightning payment", hash, user.username, amount); if (seen.includes(hash)) { l.warn("attempted to pay a paid invoice", user.username); diff --git a/lib/sockets.js b/lib/sockets.js index 514d701a..117a8d67 100644 --- a/lib/sockets.js +++ b/lib/sockets.js @@ -6,10 +6,21 @@ wss = new WebSocket.Server({ clientTracking: false, noServer: true }); /* eslint-disable-next-line */ const pick = (O, ...K) => K.reduce((o, k) => ((o[k] = O[k]), o), {}); +const subscriptions = []; emit = (username, type, data) => { if (data.username !== undefined) data = pick(data, ...whitelist); + if (type === "payment" && data.amount > 0) { + for (let i = subscriptions.length - 1; i >= 0; i--) { + let s = subscriptions[i]; + if (s.invoice.amount === data.amount) { + s.ws.send(JSON.stringify({ type, data })); + subscriptions.splice(i, 1); + } + } + } + if (!sockets[username]) return; for (id in sockets[username]) { const ws = sockets[username][id]; @@ -46,18 +57,17 @@ wss.on("connection", function (ws, req) { l.error("coudn't parse socket message"); } - let token; switch (type) { case "login": - token = data; + let token = data; if (!token || token === "null") return; try { const username = jwt.decode(token).username; if (!sockets[username]) sockets[username] = {}; const existing = Object.keys(sockets[username]); if (existing.length > 5) { - l.warn("too many sockets, logging out", existing[5]); - sockets[username][existing[5]].send( + l.warn("too many sockets, logging out", existing[3]); + sockets[username][existing[3]].send( JSON.stringify({ type: "login", data: false }) ); } @@ -88,7 +98,6 @@ wss.on("connection", function (ws, req) { ws.send( JSON.stringify({ type: "version", data: config.clientVersion }) ); - l.info("socket open", username, id); } catch (e) { ws.close(); l.error( @@ -101,9 +110,8 @@ wss.on("connection", function (ws, req) { } break; case "token": - token = data; if (!ws.user) return; - ws.token = token; + ws.token = data; break; case "lnurl": sessions[data] = ws; @@ -125,6 +133,9 @@ wss.on("connection", function (ws, req) { emit(ws.user.username, "payment", payment); } break; + case "subscribe": + subscriptions.push({ invoice: data, ws }); + break; default: l.warn("received socket message of unknown type", type, data); } @@ -135,7 +146,6 @@ wss.on("connection", function (ws, req) { const username = names[id]; if (sockets[username] && sockets[username][id]) delete sockets[username][id]; - l.info("socket closed", username, id); } catch (e) { l.error("problem closing socket", e.message); } diff --git a/routes/invoices.js b/routes/invoices.js index fdfb39d8..f344e9f9 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -14,14 +14,23 @@ app.get("/invoice", auth, ah(async (req, res, next) => { } })); -app.post("/invoice", auth, ah(async (req, res, next) => { - const { invoice } = req.body; - invoice.user_id = req.user.id; +app.post("/invoice", ah(async (req, res, next) => { + let { invoice, user } = req.body; + if (!user) ({ user } = req); + else { + user = await db.User.findOne({ + where: { + username: user.username, + } + }); + } + if (!user) throw new Error("user not provided"); + invoice.user_id = user.id; l.info( "creating invoice", invoice.text, - req.user.username, + user.username, invoice.network, invoice.amount, invoice.tip, @@ -44,7 +53,7 @@ app.post("/invoice", auth, ah(async (req, res, next) => { }, }); - res.send( - exists ? await exists.update(invoice) : await db.Invoice.create(invoice) - ); + invoice = exists ? await exists.update(invoice) : await db.Invoice.create(invoice); + addresses[invoice.address] = user.username; + res.send(invoice); })); diff --git a/routes/lightning/invoice.js b/routes/lightning/invoice.js index 689c696a..51eff332 100644 --- a/routes/lightning/invoice.js +++ b/routes/lightning/invoice.js @@ -1,8 +1,6 @@ module.exports = ah(async (req, res) => { const { amount, memo, tip } = req.body; - l.info("adding lightning invoice", req.user.username, amount, tip); - try { if (config.lna.clightning) { const invoice = await lnb.invoice(`${amount + tip}sat` || "any", new Date(), memo, 360); diff --git a/routes/lightning/receive.js b/routes/lightning/receive.js index 34ac1fc3..e8246ab9 100644 --- a/routes/lightning/receive.js +++ b/routes/lightning/receive.js @@ -1,90 +1,97 @@ const asset = "LNBTC"; -const handlePayment = async msg => { +const handlePayment = async (msg) => { try { - await db.transaction(async transaction => { - if (!msg.settled) return; - - const invoice = await db.Invoice.findOne({ - where: { - text: msg.payment_request - } - }); - - if (!invoice) return l.warn("received lightning with no invoice", msg.payment_request); - - const { text: hash, currency, memo, rate, tip, user_id } = invoice; - const amount = parseInt(msg.amt_paid_sat) - tip; - - const account = await db.Account.findOne({ - where: { - user_id, - asset: config.liquid.btcasset - }, - include: { - model: db.User, - as: 'user', - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - const { user } = account; - - let preimage = msg.r_preimage.toString("hex"); - - let payment = await db.Payment.create({ - account_id: account.id, - user_id, - hash, - memo, - amount, - currency, - preimage, - rate, - received: true, - confirmed: true, - network: "LNBTC", - tip - }, - { transaction }); - - let total = amount + tip; - invoice.received += total; - account.balance += total; - - await account.save({ transaction }); - await invoice.save({ transaction }); - await payment.save({ transaction }); - payments.push(msg.payment_request); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - emit(user.username, "payment", payment); - emit(user.username, "account", payment.account); - notify(user, `Received ${total} SAT`); - - l.info( - "lightning payment received", - user.username, - payment.amount, - payment.tip - ); - }); - } catch(e) { + await db.transaction(async (transaction) => { + if (!msg.settled) return; + + const invoice = await db.Invoice.findOne({ + where: { + text: msg.payment_request, + }, + }); + + if (!invoice) + return l.warn( + "received lightning with no invoice", + msg.payment_request + ); + + const { text: hash, currency, memo, rate, tip, user_id } = invoice; + const amount = parseInt(msg.amt_paid_sat) - tip; + + const account = await db.Account.findOne({ + where: { + user_id, + asset: config.liquid.btcasset, + }, + include: { + model: db.User, + as: "user", + }, + lock: transaction.LOCK.UPDATE, + transaction, + }); + + const { user } = account; + + let preimage = msg.r_preimage.toString("hex"); + + let payment = await db.Payment.create( + { + account_id: account.id, + user_id, + hash, + memo, + amount, + currency, + preimage, + rate, + received: true, + confirmed: true, + network: "LNBTC", + tip, + }, + { transaction } + ); + + let total = amount + tip; + invoice.received += total; + account.balance += total; + + await account.save({ transaction }); + await invoice.save({ transaction }); + await payment.save({ transaction }); + payments.push(msg.payment_request); + + payment = payment.get({ plain: true }); + payment.account = account.get({ plain: true }); + + emit(user.username, "payment", payment); + emit(user.username, "account", payment.account); + notify(user, `Received ${total} SAT`); + + l.info( + "lightning payment received", + user.username, + payment.amount, + payment.tip + ); + }); + } catch (e) { l.error("problem receiving lightning payment", e.message); - } + } }; if (config.lna.clightning) { - const poll = async ln => { - const wait = async i => { + const poll = async (ln) => { + const wait = async (i) => { const { bolt11: payment_request, pay_index, status, msatoshi_received, - payment_preimage: r_preimage + payment_preimage: r_preimage, } = await ln.waitanyinvoice(i); let settled = status === "paid"; @@ -94,13 +101,13 @@ if (config.lna.clightning) { payment_request, settled, amt_paid_sat, - r_preimage + r_preimage, }); wait(pay_index); }; const { invoices } = await ln.listinvoices(); - wait(Math.max(...invoices.map(i => i.pay_index).filter(n => n))); + wait(Math.max(...invoices.map((i) => i.pay_index).filter((n) => n))); }; poll(lna); diff --git a/routes/lnurl.js b/routes/lnurl.js index d8336709..44eb0fb9 100644 --- a/routes/lnurl.js +++ b/routes/lnurl.js @@ -17,186 +17,217 @@ var optionalAuth = function (req, res, next) { })(req, res, next); }; -app.get("/url", ah(async (req, res, next) => { - try { - const code = await db.Code.findOne({ - where: { - code: req.query.code, - }, - }); +app.get( + "/url", + ah(async (req, res, next) => { + try { + const code = await db.Code.findOne({ + where: { + code: req.query.code, + }, + }); - if (code) { - res.send(code.text); - } else { - throw new Error("code not found"); + if (code) { + res.send(code.text); + } else { + throw new Error("code not found"); + } + } catch (e) { + l.error("couldn't find url", e.message); } - } catch (e) { - l.error("couldn't find url", e.message); - } -})); + }) +); -app.get("/withdraw", auth, ah(async (req, res, next) => { - const { min, max } = req.query; - try { - const result = await lnurlServer.generateNewUrl("withdrawRequest", { - minWithdrawable: min * 1000, - maxWithdrawable: max * 1000, - defaultDescription: "coinos voucher", - }); +app.get( + "/withdraw", + auth, + ah(async (req, res, next) => { + const { min, max } = req.query; + try { + const result = await lnurlServer.generateNewUrl("withdrawRequest", { + minWithdrawable: min * 1000, + maxWithdrawable: max * 1000, + defaultDescription: "coinos voucher", + }); - withdrawals[result.secret] = req.user; + withdrawals[result.secret] = req.user; - res.send(result); - } catch (e) { - l.error("problem generating withdrawl url", e.message); - res.status(500).send(e.message); - } -})); + res.send(result); + } catch (e) { + l.error("problem generating withdrawl url", e.message); + res.status(500).send(e.message); + } + }) +); -app.post("/code", ah(async (req, res, next) => { - try { - const { encoded } = req.body.lnurl; - const code = await db.Code.create({ - code: "lnurl:" + encoded.substr(-32), - text: encoded, - }); +app.post( + "/code", + ah(async (req, res, next) => { + try { + const { encoded } = req.body.lnurl; + const code = await db.Code.create({ + code: "lnurl:" + encoded.substr(-32), + text: encoded, + }); - res.send(code); - } catch (e) { - res.status(500).send(e.message); - } -})); - -app.post("/withdraw", auth, ah(async (req, res, next) => { - const { user } = req; - const { - amount: value, - params: { callback, k1 }, - } = req.body; - const invoice = await lna.addInvoice({ value }); - const { payment_request: pr } = invoice; - const url = `${callback}?k1=${k1}&pr=${pr}`; - - await db.Invoice.create({ - user_id: user.id, - text: pr, - rate: app.get("rates")[user.currency], - currency: user.currency, - amount: value, - tip: 0, - network: "BTC", - }); + res.send(code); + } catch (e) { + res.status(500).send(e.message); + } + }) +); - try { - result = (await axios.get(url)).data; - res.send(result); - } catch (e) { - l.error("failed to withdraw", e.message); - res.status(500).send(e.message); - } -})); +app.post( + "/withdraw", + auth, + ah(async (req, res, next) => { + const { user } = req; + const { + amount: value, + params: { callback, k1 }, + } = req.body; + const invoice = await lna.addInvoice({ value }); + const { payment_request: pr } = invoice; + const url = `${callback}?k1=${k1}&pr=${pr}`; + + await db.Invoice.create({ + user_id: user.id, + text: pr, + rate: app.get("rates")[user.currency], + currency: user.currency, + amount: value, + tip: 0, + network: "BTC", + }); -const pay = async ({ amount, minSendable, maxSendable }, res, user) => { - minSendable = minSendable || 1000; - maxSendable = maxSendable || 1000000000; - if (parseInt(amount)) minSendable = maxSendable = amount * 1000; + try { + result = (await axios.get(url)).data; + res.send(result); + } catch (e) { + l.error("failed to withdraw", e.message); + res.status(500).send(e.message); + } + }) +); - try { - const result = await lnurlServer.generateNewUrl("payRequest", { - minSendable, - maxSendable, - metadata: JSON.stringify([["text/plain", `paying ${user.username}`]]), +app.get( + "/pay/:username", + ah(async (req, res, next) => { + const { username } = req.params; + let user = await db.User.findOne({ + where: { + username, + }, }); - recipients[result.secret] = user; - l.info("recipient", user.username, result.secret); - res.send(result); - } catch (e) { - l.error("problem generating payment url", e.message); - res.status(500).send(e.message); - } -}; + let { amount, minSendable, maxSendable } = req.query; + minSendable = minSendable || 1000; + maxSendable = maxSendable || 1000000000; + if (parseInt(amount)) minSendable = maxSendable = amount * 1000; -app.get("/pay/:username", ah(async (req, res, next) => { - const { username } = req.params; - let user = await db.User.findOne({ - where: { - username, - }, - }); - pay(req.query, res, user); -})); - -app.get("/pay", auth, ah(async (req, res, next) => { - const { user } = req; - pay(req.query, res, user); -})); - -app.post("/pay", auth, ah(async (req, res, next) => { - const { user } = req; - const { - amount, - params: { callback, k1 }, - } = req.body; - - const url = `${callback}?amount=${amount * 1000}`; - l.info("url", url); + let invoices = await db.Invoice.findAll({ + where: { + amount, + }, + order: [["id", "DESC"]], + limit: 1, + }); - try { - const parts = callback.split("/"); - const secret = parts[parts.length - 1]; - payments[secret] = user; + if (invoices.length) await invoices[0].destroy(); - const result = (await axios.get(url)).data; - res.send(await send(amount, '', result.pr, user)); - } catch (e) { - l.error("failed to send payment", e.message); - res.status(500).send(e.message); - } -})); + try { + const result = await lnurlServer.generateNewUrl("payRequest", { + minSendable, + maxSendable, + metadata: JSON.stringify([["text/plain", `paying ${user.username}`]]), + }); -app.get("/login", optionalAuth, ah(async (req, res, next) => { - try { - const result = await lnurlServer.generateNewUrl("login"); + recipients[result.secret] = user; + l.info("recipient", user.username, result.secret); + res.send(result); + } catch (e) { + l.error("problem generating payment url", e.message); + res.status(500).send(e.message); + } + }) +); - if (req.user) { - logins[result.secret] = req.user.username; +app.post( + "/pay", + auth, + ah(async (req, res, next) => { + const { user } = req; + const { + amount, + params: { callback, k1 }, + } = req.body; + + const url = `${callback}?amount=${amount * 1000}`; + + try { + const parts = callback.split("/"); + const secret = parts[parts.length - 1]; + payments[secret] = user; + + const result = (await axios.get(url)).data; + res.send(await send(amount, "", result.pr, user)); + } catch (e) { + l.error("failed to send payment", e.message); + throw e; + res.status(500).send(e.message); } + }) +); - res.send(result); - } catch (e) { - l.error("problem generating login url", e.message); - res.status(500).send(e.message); - } -})); +app.get( + "/login", + optionalAuth, + ah(async (req, res, next) => { + try { + const result = await lnurlServer.generateNewUrl("login"); -app.get("/decode", ah(async (req, res, next) => { - const { text } = req.query; + if (req.user) { + logins[result.secret] = req.user.username; + } - try { - const url = lnurl.decode(text); - let spl = url.split("?"); - if (spl.length > 1 && qs.parse(spl[1]).tag === "login") { - return res.send({ - tag: "login", - k1: qs.parse(spl[1]).k1, - callback: url, - domain: url - .split("://")[1] - .split("/")[0] - .split("@") - .slice(-1)[0] - .split(":")[0], - }); + res.send(result); + } catch (e) { + l.error("problem generating login url", e.message); + res.status(500).send(e.message); } + }) +); - const { data: params } = await axios.get(url); - res.send(params); - } catch (e) { - l.error("problem decoding lnurl", e.message); - res.status(500).send(e.message); - } -})); +app.get( + "/decode", + ah(async (req, res, next) => { + const { text } = req.query; + + try { + const url = lnurl.decode(text); + let spl = url.split("?"); + if (spl.length > 1 && qs.parse(spl[1]).tag === "login") { + return res.send({ + tag: "login", + k1: qs.parse(spl[1]).k1, + callback: url, + domain: url + .split("://")[1] + .split("/")[0] + .split("@") + .slice(-1)[0] + .split(":")[0], + }); + } + + const { data: params } = await axios.get(url); + res.send(params); + } catch (e) { + l.error("problem decoding lnurl", e.message); + res.status(500).send(e.message); + } + }) +); lnurlServer.bindToHook( "middleware:signedLnurl:afterCheckSignature", @@ -221,36 +252,29 @@ lnurlServer.bindToHook( if (account.balance < amount) { throw new Error("Insufficient funds"); } - - l.info("lnurl payment", user.username, amount); } const recipient = recipients[secret]; if (recipient) { - setTimeout(async () => { - try { - let { invoices } = await lnb.listInvoices({ - pending_only: true, - num_max_invoices: 1, - reversed: true, - }); - let i = invoices.find((i) => parseInt(i.value) === amount); - if (i) { - await db.Invoice.create({ - user_id: recipient.id, - text: i.payment_request, - rate: app.get("rates")[recipient.currency], - currency: recipient.currency, - amount: Math.round(amount / 1000), - tip: 0, - network: "BTC", - }); - } - } catch (e) { - l.error("problem trying to get create payment invoice", e); - } - }, 1000); + setTimeout(async () => { + let { invoices } = await lnb.listInvoices({ + pending_only: true, + }); + + if (invoices.length) { + let invoice = await db.Invoice.create({ + user_id: recipient.id, + text: invoices[invoices.length - 1].payment_request, + rate: app.get("rates")[recipient.currency], + currency: recipient.currency, + amount, + tip: 0, + network: "BTC", + }); + l.info("invoice created", invoice.text, invoice.amount); + } + }, 100); } } @@ -337,6 +361,7 @@ lnurlServer.bindToHook( let p = payments.find((p) => p.payment_request === pr); if (p) { + l.info("found payment", pr); payment.fee = p.fee; account.balance -= p.fee; await account.save(); diff --git a/routes/payments.js b/routes/payments.js index dcd76f2e..d88c95a7 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -95,7 +95,7 @@ const read = require("../lib/read"); "/lightning/channelRequest", require("./lightning/channelRequest") ); - app.post("/lightning/invoice", auth, require("./lightning/invoice")); + app.post("/lightning/invoice", require("./lightning/invoice")); app.post("/lightning/query", auth, require("./lightning/query")); app.post("/lightning/send", auth, require("./lightning/send")); require("./lightning/receive"); diff --git a/routes/users.js b/routes/users.js index 1a0f82c1..471a4e5e 100644 --- a/routes/users.js +++ b/routes/users.js @@ -294,23 +294,15 @@ app.post( }) ); -app.post( +app.get( "/address", - auth, ah(async (req, res) => { if (!config.bitcoin) return res.status(500).send("Bitcoin not configured"); - let { user } = req; - delete addresses[user.address]; - if (config.bitcoin.walletpass) await bc.walletPassphrase(config.bitcoin.walletpass, 300); - user.address = await bc.getNewAddress("", req.body.type || "bech32"); - await user.save(); - addresses[user.address] = user.username; - emit(user.username, "user", user); - res.send(user.address); + res.send(await bc.getNewAddress("", req.query.type || "bech32")); }) ); -- cgit v1.2.3