diff options
| -rw-r--r-- | routes/swaps.js | 6 | ||||
| -rw-r--r-- | swaps.js | 876 |
2 files changed, 3 insertions, 879 deletions
diff --git a/routes/swaps.js b/routes/swaps.js index 8872face..9e1fd1ea 100644 --- a/routes/swaps.js +++ b/routes/swaps.js @@ -135,16 +135,16 @@ const swap = async (user, { a1, a2, v1, v2 }) => { assets["a0c358a0f6947864af3a06f3f6a2aeb304df7fd95c922f2f22d7412399ce7691"] = "adamcoin"; - if (!assets[a1]) throw new Error("unsupported asset"); + if (!assets[a1]) throw new Error("Asset not found on server"); if (v1 > Math.round(b[assets[a1]] * SATS)) - throw new Error(`insufficient server funds, ${v1} ${b[assets[a1]]}`); + throw new Error(`Insufficient server funds, ${v1} ${b[assets[a1]]}`); await db.transaction(async transaction => { let a1acc = await getAccount(a1, user, transaction); let a2acc = await getAccount(a2, user, transaction); if (v1 > a1acc.balance) - throw new Error(`insufficient funds, ${v1} ${a1acc.balance}`); + throw new Error(`Insufficient funds, ${v1} ${a1acc.balance}`); l.info( `trade ${v1} ${a1.substr(0, 6)} for ${v2} ${a2.substr(0, 6)}`, diff --git a/swaps.js b/swaps.js deleted file mode 100644 index 89fc551a..00000000 --- a/swaps.js +++ /dev/null @@ -1,876 +0,0 @@ -const axios = require("axios"); -const crypto = require("crypto"); -const level = require("level"); -const fs = require("fs"); -const { spawn } = require("child_process"); -const leveldb = level("leveldb"); -const getAccount = require("../lib/account"); -const { Op, col } = require("sequelize"); -const shallow = a => { - let b = {}; - for (x in a) { - if (a[x] instanceof Date || typeof a[x] !== "object") b[x] = a[x]; - } - return b; -}; - -const assets = { - b2e15d0d7a0c94e4e2ce0fe6e8691b9e451377f6e46e8045a86f7c4b5d4f0f23: "bitcoin", - "4eebe36eb0819e6daa5dd3c97733251ff4eb728c810d949365d6dacaad5ef6e8": "tether" -}; - -const swapsdir = "swaps/"; -if (!fs.existsSync(swapsdir)) { - fs.mkdirSync(swapsdir); -} - -const timeout = 20000; - -const cli = (...args) => { - const params = ["--regtest", "-c", config.liquid.conf, ...args]; - if (config.liquid.network !== "regtest") params.shift(); - l.info("spawning liquidswap-cli with params:", params); - return spawn("liquidswap-cli", params); -}; - -const getInfo = filename => - new Promise((resolve, reject) => { - const proc = cli("info", swapsdir + filename); - - proc.stdout.on("data", data => { - resolve(data.toString()); - }); - - proc.stderr.on("data", err => { - l.error("info error", err.toString()); - reject(err.toString()); - }); - - proc.on("close", (code, signal) => { - let msg = (code && code.toString()) || (signal && signal.toString()); - reject(new Error(`Liquid swap tool process closed unexpectedly: ${msg}`)); - }); - - proc.on("exit", (code, signal) => { - let msg = (code && code.toString()) || (signal && signal.toString()); - reject(new Error(`Liquid swap tool process exited unexpectedly: ${msg}`)); - }); - - setTimeout( - () => reject(new Error("Liquid swap tool timed out"), proc), - timeout - ); - }); - -const swap = async (user, { a1, a2, v1, v2 }) => { - let rate = v2 / v1; - - const b = await lq.getBalance(); - - Object.keys(b).map(asset => { - assets[asset] = asset; - }); - - assets[config.liquid.btcasset] = "bitcoin"; - assets["ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2"] = - "tether"; - assets["a0c358a0f6947864af3a06f3f6a2aeb304df7fd95c922f2f22d7412399ce7691"] = - "adamcoin"; - - if (!assets[a1]) throw new Error("unsupported asset"); - if (v1 > Math.round(b[assets[a1]] * SATS)) - throw new Error(`insufficient server funds, ${v1} ${b[assets[a1]]}`); - - await db.transaction(async transaction => { - let a1acc = await getAccount(a1, user, transaction); - let a2acc = await getAccount(a2, user, transaction); - - if (v1 > a1acc.balance) - throw new Error(`insufficient funds, ${v1} ${a1acc.balance}`); - - l.info( - `trade ${v1} ${a1.substr(0, 6)} for ${v2} ${a2.substr(0, 6)}`, - user.username - ); - - await a1acc.decrement({ balance: v1 }, { transaction }); - await a1acc.reload({ transaction }); - - let payment = await db.Payment.create( - { - hash: "Trade Funding", - amount: -v1, - account_id: a1acc.id, - user_id: user.id, - currency: user.currency, - rate: app.get("rates")[user.currency], - confirmed: true, - received: false, - network: "COINOS" - }, - { transaction } - ); - - emit(user.username, "payment", payment.get({ plain: true })); - emit(user.username, "account", a1acc.get({ plain: true })); - - const orders = await db.Order.findAll( - { - where: { - "$acc1.asset$": a2, - "$acc2.asset$": a1, - rate: { [Op.lte]: 1/rate }, - accepted: false - }, - order: [ - ["rate", "ASC"], - ["id", "ASC"] - ], - include: [ - { - model: db.User, - as: "user" - }, - { - model: db.Account, - as: "acc1" - }, - { - model: db.Account, - as: "acc2" - } - ] - }, - { transaction } - ); - - for (let i = 0; i < orders.length; i++) { - if (!v1) break; - p = orders[i]; - if (p.v2 > v1) { - p.v2 -= v1; - await p.save({ transaction }); - - let payment = await db.Payment.create( - { - hash: "Trade Fill", - amount: v2, - account_id: p.a1_id, - user_id: p.user_id, - currency: p.user.currency, - rate: app.get("rates")[p.user.currency], - confirmed: true, - received: true, - network: "COINOS" - }, - { transaction } - ); - - await p.acc1.increment({ balance: v2 }, { transaction }); - await p.acc1.reload({ transaction }); - - emit(p.user.username, "payment", payment.get({ plain: true })); - emit(p.user.username, "account", p.acc1.get({ plain: true })); - - payment = await db.Payment.create( - { - hash: "Trade Fill", - amount: v1, - account_id: a1acc.id, - user_id: user.id, - currency: user.currency, - rate: app.get("rates")[user.currency], - confirmed: true, - received: true, - network: "COINOS" - }, - { transaction } - ); - - a1acc.increment({ balance: v1 }, { transaction }); - await a1acc.reload({ transaction }); - - emit(user.username, "payment", payment.get({ plain: true })); - emit(user.username, "account", a1acc.get({ plain: true })); - - if (p.fee) { - const btc = await getAccount( - config.liquid.btcasset, - p.user, - transaction - ); - - payment = await db.Payment.create( - { - hash: "Swap Fee Refund", - amount: p.fee, - account_id: btc.id, - user_id: p.user_id, - currency: p.user.currency, - rate: app.get("rates")[p.user.currency], - confirmed: true, - received: true, - network: "COINOS" - }, - { transaction } - ); - - await btc.increment({ balance: p.fee }, { transaction }); - await btc.reload({ transaction }); - - emit(p.user.username, "account", btc.get({ plain: true })); - } - - emit(p.user.username, "payment", payment.get({ plain: true })); - - p = p.get({ plain: true }); - p.a1 = p.acc1.asset; - p.a2 = p.acc2.asset; - broadcast("order", shallow(p)); - v1 = 0; - } else { - v1 -= p.v2; - v2 = Math.round(v1 * rate); - - p.accepted = true; - p.completedAt = new Date(); - - await p.save({ transaction }); - - let payment = await db.Payment.create( - { - hash: "Trade Fill", - amount: p.v1, - account_id: a2acc.id, - user_id: user.id, - currency: user.currency, - rate: app.get("rates")[user.currency], - confirmed: true, - received: true, - network: "COINOS" - }, - { transaction } - ); - - await a2acc.increment({ balance: p.v1 }, { transaction }); - await a2acc.reload({ transaction }); - - emit(user.username, "payment", payment.get({ plain: true })); - emit(user.username, "account", a2acc.get({ plain: true })); - - payment = await db.Payment.create( - { - hash: "Trade Fill", - amount: p.v2, - account_id: p.a2_id, - user_id: p.user_id, - currency: p.user.currency, - rate: app.get("rates")[p.user.currency], - confirmed: true, - received: true, - network: "COINOS" - }, - { transaction } - ); - - emit(p.user.username, "payment", payment.get({ plain: true })); - await p.acc2.increment({ balance: p.v2 }, { transaction }); - await p.acc2.reload({ transaction }); - - p = p.get({ plain: true }); - p.a1 = p.acc1.asset; - p.a2 = p.acc2.asset; - broadcast("order", shallow(p)); - } - } - - let order; - if (v1) { - order = await db.Order.create( - { - v1, - v2, - user_id: user.id, - a1_id: a1acc.id, - a2_id: a2acc.id - }, - { transaction } - ); - - const text = await new Promise((resolve, reject) => { - l.info( - "running liquid-cli tool %s %s %s %s %s", - config.liquid.conf, - a1, - v1, - a2, - v2 - ); - const proc = cli( - "propose", - a1, - (v1 / SATS).toFixed(8), - a2, - (v2 / SATS).toFixed(8) - ); - - proc.stdout.on("data", data => { - resolve(data.toString()); - }); - - proc.stderr.on("error", err => { - l.error("order error", err.toString()); - reject(err.toString()); - }); - - proc.on("close", (code, signal) => { - let msg = (code && code.toString()) || (signal && signal.toString()); - reject( - new Error(`Liquid swap tool process closed unexpectedly: ${msg}`) - ); - }); - - proc.on("exit", (code, signal) => { - let msg = (code && code.toString()) || (signal && signal.toString()); - reject( - new Error(`Liquid swap tool process exited unexpectedly: ${msg}`) - ); - }); - - setTimeout( - () => reject(new Error("Liquid swap tool timed out"), proc), - timeout - ); - }); - - const filename = `order-${order.id}.txt`; - - fs.writeFileSync(swapsdir + filename, text); - let info = JSON.parse(await getInfo(filename)); - let [fee, rate, asset] = parse(info); - - const btc = await getAccount(config.liquid.btcasset, user, transaction); - fee = Math.round(fee * SATS); - await btc.decrement({ balance: fee }, { transaction }); - await btc.save({ transaction }); - emit(user.username, "account", btc.get({ plain: true })); - - let payment = await db.Payment.create( - { - hash: "Swap Fee Deposit", - amount: -fee, - account_id: btc.id, - user_id: user.id, - currency: user.currency, - rate: app.get("rates")[user.currency], - confirmed: true, - received: false, - network: "COINOS" - }, - { transaction } - ); - - emit(user.username, "payment", payment.get({ plain: true })); - - order.fee = fee; - order.text = text; - await order.save({ transaction }); - - order = order.get({ plain: true }); - order.rate = v2 / v1; - order.a1 = a1; - order.a2 = a2; - - broadcast("order", order); - } - }); -}; - -const cancel = async (user, id) => { - await db.transaction(async transaction => { - let order = await db.Order.findOne( - { - where: { - id, - user_id: user.id - }, - include: { - model: db.Account, - as: "acc1" - } - }, - - { transaction } - ); - - let { acc1: account, v1, v2 } = order; - await account.increment({ balance: v1 }, { transaction }); - await account.reload({ transaction }); - - let rate = v2 / v1; - let payment = await db.Payment.create( - { - hash: "Trade Cancelled", - amount: v1, - account_id: account.id, - user_id: user.id, - currency: user.currency, - rate: app.get("rates")[user.currency], - confirmed: true, - received: true, - network: "COINOS" - }, - { transaction } - ); - - emit(user.username, "account", account.get({ plain: true })); - emit(user.username, "payment", payment.get({ plain: true })); - - if (order.fee) { - const btc = await getAccount(config.liquid.btcasset, user, transaction); - - payment = await db.Payment.create( - { - hash: "Swap Fee Refund", - amount: order.fee, - account_id: btc.id, - user_id: user.id, - currency: user.currency, - rate: app.get("rates")[user.currency], - confirmed: true, - received: true, - network: "COINOS" - }, - { transaction } - ); - - await btc.increment({ balance: order.fee }, { transaction }); - await btc.reload({ transaction }); - - emit(user.username, "account", btc.get({ plain: true })); - emit(user.username, "payment", payment.get({ plain: true })); - } - - broadcast("removeOrder", id); - await order.destroy({ transaction }); - }); -}; - -app.delete( - "/order/:id", - auth, - ah(async (req, res) => { - const { id } = req.params; - const { user } = req; - - await cancel(user, id); - res.end(); - }) -); - -app.post( - "/orders", - auth, - ah(async (req, res) => { - const { user } = req; - try { - await swap(user, req.body); - res.end(); - } catch (e) { - l.error(req.user.username, e.message, e.stack); - res.status(500).send(e.message); - } - }) -); - -app.post( - "/accept", - optionalAuth, - ah(async (req, res) => { - try { - const { id, text } = req.body; - - let order = await db.Order.findOne({ - where: { - id - }, - include: { - model: db.User, - as: "user" - } - }); - - if (!order) throw new Error("Order not found"); - - const { user } = req; - - if (text) { - const filename = `acceptance-${id}.txt`; - fs.writeFileSync(swapsdir + filename, text); - - let info = JSON.parse(await getInfo(filename)); - let [fee, rate, asset] = parse(info); - let { tx, u_address_p, u_address_r } = JSON.parse( - Buffer.from(text, "base64").toString() - ); - const sha256 = crypto.createHash("sha256"); - sha256.update(tx); - const hash = sha256.digest("hex"); - - const leg1 = info.legs.find(leg => !leg.incoming); - const leg2 = info.legs.find(leg => leg.incoming); - - await db.transaction(async transaction => { - const l2a2 = await getAccount(leg2.asset, order.user, transaction); - - let amount = Math.round(leg1.amount * SATS); - fee = Math.round(fee * SATS); - - amount = Math.round(leg2.amount * SATS); - - await db.Invoice.create({ - user_id: order.user_id, - text: u_address_p, - address: u_address_p, - unconfidential: u_address_p, - currency: order.user.currency, - memo: "Atomic Swap", - rate: app.get("rates")[order.user.currency], - amount, - tip: 0, - network: "liquid", - account_id: l2a2.id - }); - - addresses[u_address_p] = order.user.username; - - await finalize(filename); - - order.accepted = true; - order.completedAt = new Date(); - await order.save({ transaction }); - - order = order.get({ plain: true }); - order.a1 = leg1.asset; - order.a2 = leg2.asset; - broadcast("order", shallow(order)); - }); - - res.send(info); - } else { - throw new Error("No acceptance provided"); - } - } catch (e) { - l.error(e.message); - res.status(500).send(e.message); - } - }) -); - -app.post( - "/acceptance", - ah(async (req, res) => { - const { acceptance: text } = req.body; - l.info("acceptance received"); - - fs.writeFile(swapsdir + "accepted.txt", text, async err => { - if (err) { - return res.status(500).send(err); - } - - try { - let info = JSON.parse( - await new Promise((resolve, reject) => { - const proc = cli("info", swapsdir + "accepted.txt"); - - proc.stdout.on("data", data => { - resolve(data.toString()); - }); - - proc.stderr.on("data", err => { - reject(err.toString()); - }); - - setTimeout( - () => reject(new Error("Liquid swap tool timed out"), proc), - timeout - ); - }) - ); - - const time = Math.floor(new Date()).toString(); - const [fee, rate, asset] = parse(info); - if (!asset) throw new Error("Unsupported asset"); - l.info("accepted", info, rate, asset); - - leveldb.put(time, JSON.stringify({ text, info, rate, asset })); - l.info({ text, info, rate, asset }); - - res.send({ info, rate }); - } catch (e) { - l.error(e); - res.status(500).send(e.message); - } - }); - }) -); - -checkQueue = async () => { - const ws = app.get("ws"); - const txs = []; - - leveldb - .createReadStream() - .on("data", async function(data) { - txs.push({ ...JSON.parse(data.value), key: data.key }); - }) - .on("end", async function(data) { - const rate = (a, b) => a.rate - b.rate; - const time = (a, b) => b.time - a.time; - - const pending = txs.filter(tx => !tx.id); - const completed = txs.filter(tx => tx.id).sort(time); - - const bitcoin = pending - .filter(tx => tx.asset === "bitcoin") - .sort(rate) - .reverse(); - const tether = pending - .filter(tx => tx.asset === "tether") - .sort(rate) - .reverse(); - - const strip = a => - a.map(({ id, key, info, rate, time }) => ({ - id, - key, - info, - rate, - time - })); - - ws && - ws.send( - JSON.stringify({ - completed: strip(completed.slice(0, 3)), - bitcoin: strip(bitcoin.slice(0, 3)), - tether: strip(tether.slice(0, 3)) - }) - ); - - let b, t, tx; - if (bitcoin[0]) b = bitcoin[0].rate - app.get("ask"); - if (tether[0]) t = tether[0].rate - 1 / app.get("bid"); - - if (b > 0 || t > 0) { - if ((b && !t) || b > t) tx = bitcoin[0]; - if ((t && !b) || t > b) tx = tether[0]; - } - - if (tx) { - try { - tx.id = JSON.parse(await finalize(tx.text)).txid; - tx.time = Date.now(); - delete tx.text; - leveldb.put(tx.key, JSON.stringify(tx)); - } catch (e) { - l.info(e); - if (e.includes("Unexpected fees")) leveldb.del(tx.key); - if (e.includes("unsigned inputs")) leveldb.del(tx.key); - if (e.includes("insufficient fee")) leveldb.del(tx.key); - } - } - }); -}; - -const parse = info => { - const fee = info.legs[0].fee; - return [ - fee, - (info.legs[1].amount / (info.legs[0].amount + fee)).toFixed(8), - assets[info.legs[0].asset] - ]; -}; - -const finalize = (filename = "finalized.txt", text) => { - if (text) fs.writeFileSync(swapsdir + filename, text); - - return new Promise((resolve, reject) => { - const proc = cli("finalize", swapsdir + filename, "-s"); - - proc.stdout.on("data", data => { - resolve(data.toString()); - }); - - proc.stderr.on("data", err => { - reject(new Error(err.toString())); - }); - - proc.on("close", (code, signal) => { - let msg = (code && code.toString()) || (signal && signal.toString()); - reject(new Error(`Liquid swap tool process closed unexpectedly: ${msg}`)); - }); - - proc.on("exit", (code, signal) => { - let msg = (code && code.toString()) || (signal && signal.toString()); - reject(new Error(`Liquid swap tool process exited unexpectedly: ${msg}`)); - }); - - setTimeout( - () => reject(new Error("Liquid swap tool timed out"), proc), - timeout - ); - }); -}; - -app.get( - "/orders", - optionalAuth, - ah(async (req, res) => { - try { - let orders = await db.Order.findAll({ - attributes: { - include: [ - [col("acc1.asset"), "a1"], - [col("acc2.asset"), "a2"] - ] - }, - include: [ - { - model: db.Account, - as: "acc1", - attributes: [] - }, - { - model: db.Account, - as: "acc2", - attributes: [] - } - ] - }); - - res.send( - orders.map(p => { - if (p.user_id !== req.user.id) p.user_id = null; - return p; - }) - ); - } catch (e) { - res.status(500).send(e.message); - } - }) -); - -app.post( - "/publish", - auth, - ah(async (req, res) => { - try { - const { id } = req.body; - await db.Order.update( - { public: true }, - { where: { id, user_id: req.user.id } } - ); - res.end(); - } catch (e) { - res.status(500).send(e.message); - } - }) -); - -setInterval(async () => { - if (!app.get("rates")) return; - const amount = 0.01; - const btc = "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225"; - const lcad = "e749f7326d0ba155ec1d878e23458bc3f740a38bf91bbce955945ee10d6ab523"; - const { CAD, USD } = app.get("rates"); - const user = await db.User.findOne({ - where: { - username: "maker" - } - }); - - await db.transaction(async transaction => { - let p = await db.Order.findOne( - { - where: { - user_id: user.id, - "$acc1.asset$": btc, - accepted: false - }, - include: [ - { - model: db.Account, - as: "acc1" - } - ] - }, - { transaction } - ); - - let params = { - a1: btc, - a2: lcad, - v1: amount * SATS, - v2: Math.round(amount * SATS * (((app.get("ask") * CAD) / USD) * 1.01)) - }; - - if (p && p.acc1.balance > p.v1) { - p.v1 = params.v1; - p.v2 = params.v2; - await p.save(); - p = p.get({ plain: true }); - p.a1 = params.a1; - p.a2 = params.a2; - - broadcast("order", shallow(p)); - } else { - try { - await swap(user, params); - } catch(e) { - l.error("Failed to make ask", e.message); - } - } - - p = await db.Order.findOne( - { - where: { - user_id: user.id, - "$acc1.asset$": lcad, - accepted: false - }, - include: [ - { - model: db.Account, - as: "acc1" - } - ] - }, - { transaction } - ); - - params = { - a1: lcad, - a2: btc, - v1: Math.round(amount * SATS * ((app.get("bid") * CAD) / USD) * 0.99), - v2: amount * SATS - } - - if (p && p.acc1.balance > p.v1) { - p.v1 = params.v1; - p.v2 = params.v2; - await p.save(); - p = p.get({ plain: true }); - p.a1 = params.a1; - p.a2 = params.a2; - - broadcast("order", shallow(p)); - } else { - try { - await swap(user, params); - } catch(e) { - l.warn("Failed to make bid", e.message); - } - } - }); -}, 6000); |
