diff options
| -rw-r--r-- | db/models/proposals.js | 9 | ||||
| -rw-r--r-- | lib/passport.js | 8 | ||||
| -rw-r--r-- | routes/swaps.js | 210 |
3 files changed, 220 insertions, 7 deletions
diff --git a/db/models/proposals.js b/db/models/proposals.js index 350bdab8..6d7ee40e 100644 --- a/db/models/proposals.js +++ b/db/models/proposals.js @@ -82,6 +82,15 @@ const attributes = { comment: null, field: "text" }, + public: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + primaryKey: false, + autoIncrement: false, + comment: null, + field: "public" + }, accepted: { type: DataTypes.BOOLEAN, allowNull: false, diff --git a/lib/passport.js b/lib/passport.js index 1d80d9b8..8f87d42d 100644 --- a/lib/passport.js +++ b/lib/passport.js @@ -28,4 +28,12 @@ passport = Passport.use( ); auth = passport.authenticate("jwt", { session: false }); + +optionalAuth = function (req, res, next) { + passport.authenticate("jwt", { session: false }, function (err, user, info) { + req.user = user; + next(); + })(req, res, next); +}; + app.use(passport.initialize()); diff --git a/routes/swaps.js b/routes/swaps.js index 127d47ea..3a77ec19 100644 --- a/routes/swaps.js +++ b/routes/swaps.js @@ -1,3 +1,4 @@ +const axios = require("axios"); const level = require("level"); const fs = require("fs"); const { spawn } = require("child_process"); @@ -42,9 +43,9 @@ const createProposal = (a1, v1, a2, v2) => }); }); -const getInfo = () => +const getInfo = (filename = "proposal.txt") => new Promise((resolve, reject) => { - const proc = cli("info", "proposal.txt"); + const proc = cli("info", filename); proc.stdout.on("data", (data) => { resolve(data.toString()); @@ -58,6 +59,17 @@ const getInfo = () => setTimeout(() => reject("timeout"), 2000); }); +app.delete("/proposal/:id", auth, async (req, res) => { + const { id } = req.params; + await db.Proposal.destroy({ + where: { + id, + user_id: req.user.id, + }, + }); + res.end(); +}); + app.get("/proposal", auth, async (req, res) => { try { const { user } = req; @@ -71,14 +83,21 @@ app.get("/proposal", auth, async (req, res) => { "bitcoin"; if (!(assets[a1] && assets[a2])) throw new Error("unsupported assets"); - if (v1 > b[assets[a1]]) throw new Error("not enough funds"); + if (v1 > b[assets[a1]]) throw new Error("insufficient server funds"); + const account = await db.Account.findOne({ + where: { + user_id: user.id, + asset: a1, + }, + }); + if (v1 > account.balance) throw new Error("insufficient funds"); l.info( `proposal requested to swap ${v1} ${assets[a1]} for ${v2} ${assets[a2]}` ); - let proposal = await createProposal(a1, v1, a2, v2); - proposal = proposal.replace(/\s+/g, "").trim(); + let text = await createProposal(a1, v1, a2, v2); + text = text.replace(/\s+/g, "").trim(); const info = JSON.parse(await getInfo()); const [fee, rate, asset] = parse(info); @@ -87,13 +106,13 @@ app.get("/proposal", auth, async (req, res) => { if (rate < 0) throw new Error(`${assets[a1]} amount must be greater than ${fee}`); - await db.Proposal.create({ + const proposal = await db.Proposal.create({ a1, a2, v1: Math.round(v1 * SATS), v2: Math.round(v2 * SATS), user_id: user.id, - text: proposal, + text, }); res.send({ proposal, info, rate, asset }); @@ -103,6 +122,151 @@ app.get("/proposal", auth, async (req, res) => { } }); +app.post("/accept", optionalAuth, async (req, res) => { + try { + const { id, text } = req.body; + l.info("id", id); + + const proposal = await db.Proposal.findOne({ + where: { + id, + accepted: false, + }, + include: { + model: db.User, + as: "user", + }, + }); + + if (!proposal) throw new Error("Proposal not found"); + + const filename = `proposal-${id}.txt`; + + fs.writeFileSync(filename, proposal.text); + const info = JSON.parse(await getInfo(filename)); + + const { user } = req; + if (user) { + const leg1 = info.legs.find((leg) => !leg.incoming && leg.funded); + const leg2 = info.legs.find((leg) => leg.incoming && !leg.funded); + await db.transaction(async (transaction) => { + const l1a1 = await db.Account.findOne({ + where: { + user_id: user.id, + asset: leg1.asset, + }, + lock: transaction.LOCK.UPDATE, + transaction, + }); + + if (!l1a1) throw new Error("Outgoing asset not found"); + + const l1a2 = await db.Account.findOne({ + where: { + user_id: proposal.user_id, + asset: leg1.asset, + }, + lock: transaction.LOCK.UPDATE, + transaction, + }); + + let amount = Math.round(leg1.amount * SATS); + if (amount > l1a2.balance) + throw new Error("Proposer has insufficient funds"); + + l1a1.balance += amount; + l1a2.balance -= amount; + + let l2a1 = await db.Account.findOne({ + where: { + user_id: user.id, + asset: leg2.asset, + }, + lock: transaction.LOCK.UPDATE, + transaction, + }); + + if (!l2a1) { + let { asset } = leg2; + let params = { + user_id: user.id, + asset, + }; + let name = asset.substr(0, 6); + let ticker = asset.substr(0, 3).toUpperCase(); + let precision = 8; + + const assets = (await axios.get("https://assets.blockstream.info/")) + .data; + + if (assets[asset]) { + ({ ticker, precision, name } = assets[asset]); + } else { + const existing = await db.Account.findOne({ + where: { + asset, + }, + order: [["id", "ASC"]], + limit: 1, + }); + + if (existing) { + ({ ticker, precision, name } = existing); + } + } + + params = { ...params, ...{ ticker, precision, name } }; + params.balance = 0; + params.pending = 0; + l2a1 = await db.Account.create(params, { transaction }); + } + + const l2a2 = await db.Account.findOne({ + where: { + user_id: proposal.user_id, + asset: leg2.asset, + }, + lock: transaction.LOCK.UPDATE, + transaction, + }); + + amount = Math.round(leg2.amount * SATS); + if (amount > l2a1.balance) throw new Error("Insufficient funds"); + + l2a1.balance -= amount; + l2a2.balance += amount; + + await l1a1.save({ transaction }); + await l1a2.save({ transaction }); + await l2a1.save({ transaction }); + await l2a2.save({ transaction }); + + proposal.accepted = true; + await proposal.save({ transaction }); + + emit(user.username, "account", l1a1); + emit(user.username, "account", l2a1); + emit(proposal.user.username, "account", l1a2); + emit(proposal.user.username, "account", l2a2); + emit(user.username, "proposal", proposal); + emit(proposal.user.username, "proposal", proposal); + }); + } else if (text) { + const filename = `acceptance-${id}.txt`; + fs.writeFileSync(filename, text); + const info = JSON.parse(await getInfo()); + l.info("info", info); + } else { + throw new Error("no acceptance provided"); + } + + res.send(info); + } catch (e) { + l.error(e.message); + res.status(500).send(e.message); + } +}); + app.post("/acceptance", async (req, res) => { const { acceptance: text } = req.body; l.info("acceptance received"); @@ -240,3 +404,35 @@ const finalize = (text) => { setTimeout(() => reject("timeout"), 2000); }); }; + +app.get("/proposals", optionalAuth, async (req, res) => { + try { + if (req.user) { + res.send(await db.Proposal.findAll()); + } else { + res.send( + await db.Proposal.findAll({ + attributes: { exclude: ["user_id"] }, + where: { + accepted: false, + }, + }) + ); + } + } catch (e) { + res.status(500).send(e.message); + } +}); + +app.post("/publish", auth, async (req, res) => { + try { + const { id } = req.body; + await db.Proposal.update( + { public: true }, + { where: { id, user_id: req.user.id } } + ); + res.end(); + } catch (e) { + res.status(500).send(e.message); + } +}); |
