diff options
| author | Adam Soltys <[email protected]> | 2023-02-06 16:48:13 -0800 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2023-02-06 16:48:13 -0800 |
| commit | 9c667055c6367e1b2e239686e72f52995679a47e (patch) | |
| tree | 0b1d4aee51ec2d4e92d08a9d1404ba4e2ecd462a /routes | |
| parent | 15294844dbc8e73b4d1e92008a7eebc6b86b9ec5 (diff) | |
| parent | 6f5602d112b68adb7452abf3047f20c32e7725b6 (diff) | |
| download | coinos-server-9c667055c6367e1b2e239686e72f52995679a47e.tar.gz coinos-server-9c667055c6367e1b2e239686e72f52995679a47e.zip | |
Merge branch 'master' into test
Diffstat (limited to 'routes')
41 files changed, 841 insertions, 5684 deletions
diff --git a/routes/admin.js b/routes/admin.js deleted file mode 100644 index 3053d813..00000000 --- a/routes/admin.js +++ /dev/null @@ -1,566 +0,0 @@ -import express from "express"; -var router = express.Router(); - -import * as parseInput from "../scripts/apiParse"; -import Sequelize from "@sequelize/core"; -const Op = Sequelize.Op; - -/** - * @api {get} /users Retrieve user list - * @apiName getUsers - * @apiGroup Admin - * - * @apiPermission admin - * - * @apiSuccess {Object} users List of users - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "users" : [ - * { "username": 'John Doe', "email": '[email protected]', ...}, - * { "username": 'Jane Doe', "email": '[email protected]', ...} - * ] - * } - */ -router.get( - "/users", - // auth, - async (req, res) => { - var { search, starts_with, contains } = req.query; - var users = knex - .select( - "username", - "email", - "phone", - "verified", - knex.raw("LEFT(createdAt,10) as created_at") - ) - .from("users"); - - var timeCondition = parseInput.addTimeSearch(req.query, "users.createdAt"); - var userCondition = parseInput.addUserSearch(req.query); - - if (timeCondition) users = users.whereRaw(timeCondition); - if (userCondition) users = users.whereRaw(userCondition); - - // Alternative Sequelize query syntax - // await db.User.findAll({ - // attributes: ['username', 'email', 'phone', 'verified', 'createdAt'] - // }) - - const found = await users; - return res.send({ users: found }); - } -); - -/** - * @api {get} /referrals Retrieve list of referral tokens - * @apiName referrals - * @apiGroup Admin - * - * @apiPermission admin - * - * @apiSuccess returns list of referral tokens - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "referrals" : [ - * { "token": '***', "status": 'pending', sponsor: 'Adam', user: 'newUsername1', expiry: null }, - * { "token": '***', "status": 'pending', sponsor: 'Adam', user: 'newUsername2', expiry: null }, - * ] - * } - */ -router.get("/referrals", auth, async (req, res) => { - var referrals = knex - .select( - "token", - "users.username as user", - "sponsor.username as sponsor", - knex.raw("LEFT(created_at, 10) as created_at"), - "referrals.status" - ) - .from("referrals") - .leftJoin("users", "users.id", "referrals.user_id") - .leftJoin("users as sponsor", "sponsor.id", "referrals.sponsor_id"); - - // Alternative Sequelize query syntax (returns sub-tables as hash) - // var referrals = await db.Referral.findAll({ - // include: [ - // {model: db.User, as: 'user', attributes: [['username', 'user']]}, - // {model: db.User, as: 'sponsor', attributes: [['username', 'sponsor']]} - // ] - // }) - - var timeCondition = parseInput.addTimeSearch( - req.query, - "referrals.updated_at" - ); - var userCondition = parseInput.addUserSearch(req.query, "sponsor"); - - if (userCondition) { - referrals = referrals.whereRaw(userCondition); - } - if (timeCondition) { - referrals = referrals.whereRaw(timeCondition); - } - - var found = await referrals; - return res.send({ referrals: found }); -}); - -/** - * @api {get} /waiting_list Retrieve waiting_list - * @apiName waiting_list - * @apiGroup Admin - * - * @apiPermission admin - * - * @apiSuccess returns list of referral tokens - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "waiting_list" : [ - * { email: "[email protected]", phone: '604 123-4567 }, - * { email: "[email protected]", phone: '604 246-8910 }, - * ] - * } - */ -router.get( - "/waiting_list", - // auth, - async (req, res) => { - var { search, starts_with, contains } = req.query; - - var queue = knex - .select( - "waiting_list.email", - "waiting_list.phone", - "waiting_list.created_at as requested", - "users.id as current_user_id" - ) - .from("waiting_list") - .leftJoin("users", "users.email", "waiting_list.email") - .where("waiting_list.id", ">", 0); - - var timeCondition = parseInput.addTimeSearch( - req.query, - "waiting_list.updated_at" - ); - var userCondition = parseInput.addUserSearch(req.query); - - if (timeCondition) queue = queue.whereRaw(timeCondition); - if (userCondition) queue = queue.whereRaw(userCondition); - - const found = await queue; - - return res.send({ queue: found }); - } -); - -/** - * @api {get} /accounts Retrieve list of accounts - * @apiName accounts - * @apiGroup Admin - * - * @apiPermission admin - * - * @apiSuccess returns list of accounts - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "accounts" : [ - * { "user": 'Adam', "account_id": '123', balance: 500 ... }, - * ... - * ] - * } - */ -router.get("/accounts", auth, async (req, res) => { - var { nonZero, search, starts_with, contains } = req.query; - - var accounts = knex - .select( - "username", - "accounts.id as account_id", - "ticker", - "balance", - knex.raw("LEFT(accounts.createdAt, 10) as created"), - knex.raw("LEFT(accounts.updatedAt, 10) as updated") - ) - .from("accounts") - .leftJoin("users", "users.id", "accounts.user_id"); - - if (nonZero) { - accounts = accounts.where("balance", ">", 0); - } - - var timeCondition = parseInput.addTimeSearch(req.query, "accounts.updatedAt"); - var userCondition = parseInput.addUserSearch(req.query); - - if (timeCondition) accounts = accounts.whereRaw(timeCondition); - if (userCondition) accounts = accounts.whereRaw(userCondition); - - const found = await accounts; - - return res.send({ accounts: found }); -}); - -/** - * @api {get} /transactions Retrieve summary of user transactions - * - * @apiName user_transactions - * @apiGroup Admin - * - * @apiPermission admin - * - * @apiSuccess returns list of invoices, payments, orders for users - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "transactions" : [ - * { "username": 'Adam', "email": '[email protected]', invoices: 1, orders: 0, payments: 2 }, - ... - * ] - * } - */ -router.get( - "/transactions", - // auth, - async (req, res) => { - var { type, since, search, starts_with, contains } = req.query; - - var transactions = knex - .select( - "users.username", - "users.email" - // knex.raw('LEFT(created_at, 10) as created') - // knex.raw('LEFT(updatedAt, 10) as updated') - // knex.raw('COUNT(invoices.id) as invoices'), - // knex.raw('COUNT(payments.id) as payments'), - // knex.raw('COUNT(orders.id) as orders') - ) - .from("users"); - - const types = ["deposits", "withdrawals", "orders", "payments", "invoices"]; - for (var i = 0; i < types.length; i++) { - var stamp = types[i].replace(/s$/, "") + "_date"; - - transactions = transactions - .select( - knex.raw("COUNT(DISTINCT " + types[i] + ".id) as " + types[i]), - knex.raw("LEFT(MAX(" + types[i] + ".updatedAt), 10) as " + stamp) - ) - .leftJoin(types[i], "users.id", types[i] + ".user_id"); - // .havingRaw(types[i] + ' > ?', [0]) - - if (since) { - transactions = transactions.havingRaw( - "(" + stamp + " >= ? OR " + stamp + " IS NULL)", - [since] - ); - } - } - - if (since) - transactions = transactions.havingRaw( - "invoices + payments + deposits + orders > ?", - [0] - ); - - var userCondition = parseInput.addUserSearch(req.query); - if (userCondition) transactions = transactions.whereRaw(userCondition); - - transactions = transactions.groupBy("users.id"); - - const found = await transactions; - - return res.send({ transactions: found }); - } -); - -/** - * @api {get} /orders Retrieve summary of orders - * - * @apiName orders - * @apiGroup Admin - * - * @apiPermission admin - * - * @apiSuccess returns list of orders - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "orders" : [ - * { "seller": 'Adam', "buyer": 'bob' ... }, - ... - * ] - * } - */ -router.get("/orders", auth, async (req, res) => { - var { type, since, search, starts_with, contains } = req.query; - - var orders = knex - .select( - "u1.username as seller", - "u2.username as buyer", - "v1 as sell_amount", - "v2 as buy_amount", - "rate", - "a1.name as from", - "a2.name as to", - knex.raw("Left(completedAt,16) as completed") - ) - .from("orders") - .leftJoin("accounts as a1", "a1.id", "orders.a1_id") - .leftJoin("accounts as a2", "a2.id", "orders.a2_id") - .leftJoin("users as u1", "a1.user_id", "u1.id") - .leftJoin("users as u2", "a2.user_id", "u2.id"); - - var timeCondition = parseInput.addTimeSearch(req.query, "orders.updatedAt"); - if (timeCondition) orders = orders.whereRaw(timeCondition); - - var userCondition1 = parseInput.addUserSearch(req.query, "u1"); - var userCondition2 = parseInput.addUserSearch(req.query, "u2"); - - if (userCondition1) - orders = orders.where(function() { - this.whereRaw(userCondition1); - this.orWhere(knex.raw(userCondition2)); - }); - - var found = await orders; - return res.send({ orders: found }); -}); - -/** - * @api {get} /invoices Retrieve summary of invoices - * - * @apiName invoices - * @apiGroup Admin - * - * @apiPermission admin - * - * @apiSuccess returns list of invoices - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "invoices" : [ - * { "seller": 'Adam', "buyer": 'bob' ... }, - ... - * ] - * } - */ -router.get("/invoices", auth, async (req, res) => { - var { type, since, search, starts_with, contains } = req.query; - - var invoices = knex - .select( - "username", - "users.email", - "invoices.currency", - "invoices.amount", - "accounts.network", - knex.raw("Left(invoices.updatedAt,16) as updated") - ) - .from("invoices") - .leftJoin("accounts", "invoices.account_id", "accounts.id") - .leftJoin("users", "accounts.user_id", "users.id"); - - var timeCondition = parseInput.addTimeSearch(req.query, "invoices.updatedAt"); - var userCondition = parseInput.addUserSearch(req.query); - - if (timeCondition) invoices = invoices.whereRaw(timeCondition); - if (userCondition) invoices = invoices.whereRaw(userCondition); - - var found = await invoices; - return res.send({ invoices: found }); -}); - -/** - * @api {get} /deposits Retrieve summary of deposits - * - * @apiName deposits - * @apiGroup Admin - * - * @apiPermission admin - * - * @apiSuccess returns list of deposits - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "deposits" : [ - * { "seller": 'Adam', "buyer": 'bob' ... }, - ... - * ] - * } - */ -router.get("/deposits", auth, async (req, res) => { - var { type, since, search, starts_with, contains } = req.query; - - var deposits = knex - .select( - "username", - "users.email", - "amount", - "credited", - knex.raw("Left(deposits.updatedAt,16) as deposited") - ) - .from("deposits") - .leftJoin("users", "deposits.user_id", "users.id"); - - var timeCondition = parseInput.addTimeSearch(req.query, "deposits.updatedAt"); - var userCondition = parseInput.addUserSearch(req.query); - - if (timeCondition) deposits = deposits.whereRaw(timeCondition); - if (userCondition) deposits = deposits.whereRaw(userCondition); - - var found = await deposits; - return res.send({ deposits: found }); -}); - -/** - * @api {get} /withdrawals Retrieve summary of withdrawals - * - * @apiName withdrawals - * @apiGroup Admin - * - * @apiPermission admin - * - * @apiSuccess returns list of withdrawals - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "withdrawals" : [ - * { "seller": 'Adam', "buyer": 'bob' ... }, - ... - * ] - * } - */ -router.get("/withdrawals", auth, async (req, res) => { - var { type, since, search, starts_with, contains } = req.query; - - var withdrawals = knex - .select( - "username", - "users.email", - "amount", - "notes", - knex.raw("Left(withdrawals.updatedAt,16) as withdrawn") - ) - .from("withdrawals") - .leftJoin("users", "withdrawals.user_id", "users.id"); - - var timeCondition = parseInput.addTimeSearch( - req.query, - "withdrawals.updatedAt" - ); - var userCondition = parseInput.addUserSearch(req.query); - - if (timeCondition) withdrawals = withdrawals.whereRaw(timeCondition); - if (userCondition) withdrawals = withdrawals.whereRaw(userCondition); - - var found = await withdrawals; - return res.send({ withdrawals: found }); -}); - -/** - * @api {get} /payments Retrieve summary of payments - * - * @apiName payments - * @apiGroup Admin - * - * @apiPermission admin - * - * @apiSuccess returns list of payments - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "payments" : [ - * { "seller": 'Adam', "buyer": 'bob' ... }, - ... - * ] - * } -**/ -router.get("/payments", auth, async (req, res) => { - var { type, since, search, starts_with, contains } = req.query; - - var payments = knex - .select( - "username", - "users.email", - "amount", - "payments.network", - knex.raw("LEFT(hash,20) as hash"), - knex.raw("Left(payments.updatedAt,16) as deposited") - ) - .from("payments") - .leftJoin("accounts", "payments.account_id", "accounts.id") - .leftJoin("users", "accounts.user_id", "users.id"); - - var timeCondition = parseInput.addTimeSearch(req.query, "payments.updatedAt"); - var userCondition = parseInput.addUserSearch(req.query); - - if (timeCondition) payments = payments.whereRaw(timeCondition); - if (userCondition) payments = payments.whereRaw(userCondition); - - var found = await payments; - return res.send({ payments: found }); -}); - -/** - * @api {get} /user_kyc Retrieve user kyc details - * - * @apiName user_kyc - * @apiGroup Admin - * - * @apiPermission admin - * - * @apiSuccess returns list of kyc details for each user - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "users" : [ - * { "username": 'Adam', "email": '[email protected]', ... kyc_data... }, - ... - * ] - * } - */ -router.get("/kyc_required", auth, async (req, res) => { - var { since, search, starts_with, contains } = req.query; - - var threshold = "0.2"; - - var kyc = knex - .select( - knex.raw("CONCAT( FORMAT(Max(amount)/1000000, 1), ' M') as max"), - "username", - "users.email", - "users.verified as kyc_verified", - // 'payments.currency', - // 'payments.network', - // knex.raw('LEFT(hash,20) as hash'), - // knex.raw('Left(payments.updatedAt,16) as transferred') - // knex.raw( parseInput.readableDate('Max(payments.updatedAt)', 'last_changed') ) - knex.raw( - "DATE_FORMAT(Max(payments.updatedAt), ?) AS last_changed", - "%M %d, %Y" - ) - ) - .from("payments") - .leftJoin("accounts", "payments.account_id", "accounts.id") - .leftJoin("users", "payments.user_id", "users.id") // should really point to accounts - .groupBy("users.id"); - // .groupBy('payments.currency') - // .groupBy('payments.network') - - var timeCondition = parseInput.addTimeSearch(req.query, "payments.updatedAt"); - var userCondition = parseInput.addUserSearch(req.query); - - if (timeCondition) kyc = kyc.whereRaw(timeCondition); - if (userCondition) kyc = kyc.whereRaw(userCondition); - - var found = await kyc.having("max", ">=", threshold); - - return res.send({ found: found }); -}); - -export default router; diff --git a/routes/assets.js b/routes/assets.js deleted file mode 100644 index ed0d3036..00000000 --- a/routes/assets.js +++ /dev/null @@ -1,536 +0,0 @@ -import { emit } from "$lib/sockets"; -import store from "$lib/store"; -import db from "$db"; -import config from "$config"; -import app from "$app"; -import { auth } from "$lib/passport"; -import axios from "axios"; -import crypto from "crypto"; -import fs from "fs"; -import { err, l } from "$lib/logging"; -import lq from "$lib/liquid"; -import { SATS } from "$lib/utils"; - -let fetchAssets; -(fetchAssets = async () => { - try { - const { data } = await axios.get("https://assets.blockstream.info/"); - - store.assets = data; - } catch (e) { - var liquid_assets = JSON.parse(fs.readFileSync("/app/assets.json")); - if (liquid_assets) { - store.assets = liquid_assets; - console.debug("using static assets..." + e.message); - } else { - err("error fetching assets", e.message); - res.code(500).send("error fetching assets"); - } - } - - setTimeout(fetchAssets, 7200000); -})(); - -app.get("/assets", async (req, res) => { - if (store.assets) { - const accounts = await db.Account.findAll({ - // group: ["asset"] - }); - Object.keys(store.assets).map(a => { - store.assets[a].registered = true; - if (!store.assets[a].asset) - store.assets[a].asset = store.assets[a].asset_id; - if ( - (store.assets[a].ticker === "BTC" && - store.assets[a].asset !== config.liquid.btcasset) || - (store.assets[a].ticker === "EUR" && - store.assets[a].asset !== config.liquid.eurasset) || - (store.assets[a].ticker === "CAD" && - store.assets[a].asset !== config.liquid.cadasset) || - (store.assets[a].ticker === "USDt" && - store.assets[a].asset !== config.liquid.usdtasset) - ) - delete store.assets[a]; - }); - accounts.map(({ asset, name, domain, ticker, precision }) => { - if (!store.assets[asset]) - store.assets[asset] = { - asset, - name, - domain, - ticker, - precision - }; - }); - - res.send(store.assets); - } else { - console.log("error getting blockstream assets"); - res.code(500).send("Problem fetching blockstream asset registry data"); - } -}); - -app.post("/assets", auth, async (req, res) => { - try { - const sha256 = crypto.createHash("sha256"); - const { id: user_id } = req.user; - const blind = false; - - if (!user_id) throw new Error("unauthorized"); - - const { - address, - domain, - name, - asset_amount, - filename, - token_amount, - precision, - pubkey, - ticker - } = req.body; - - const asset_address = address - ? address - : await lq.getNewAddress("", "legacy"); - - const token_address = - token_amount && (await lq.getNewAddress("", "legacy")); - - const issuer_pubkey = pubkey - ? pubkey - : (await lq.getAddressInfo(asset_address)).pubkey; - const version = 0; - - let contract = { - entity: { domain }, - issuer_pubkey, - name, - precision, - version - }; - - if (filename) contract.file = filename; - if (ticker) contract.ticker = ticker; - - contract = Object.keys(contract) - .sort() - .reduce((r, k) => ((r[k] = contract[k]), r), {}); - - l( - "attempting issuance", - req.user.username, - JSON.stringify(contract), - "address", - address, - "pubkey", - pubkey - ); - - sha256.update(JSON.stringify(contract)); - const hash = sha256.digest("hex"); - const contract_hash = hash - .match(/[a-f0-9]{2}/g) - .reverse() - .join(""); - const rawtx = await lq.createRawTransaction([], [{ data: "00" }]); - const funded = await lq.fundRawTransaction(rawtx, { feeRate: 0.000002 }); - const params = { - asset_amount, - asset_address, - blind, - contract_hash - }; - - params.asset_amount = asset_amount / (SATS / 10 ** precision); - - if (token_amount) { - params.token_amount = token_amount; - params.token_address = token_address; - params.token_amount = parseInt(params.token_amount); - } - - let ria; - try { - ria = await lq.rawIssueAsset(funded.hex, [params]); - } catch (e) { - l(asset_amount, token_amount, params); - throw new Error(e.message); - } - - const { asset, hex, token } = ria[0]; - const brt = await lq.blindRawTransaction(hex, true, [], false); - const srt = await lq.signRawTransactionWithWallet(brt); - const allowed = (await lq.testMempoolAccept([srt.hex]))[0].allowed; - if (!allowed) throw new Error("issuance rejected by mempool", srt.hex); - const txid = await lq.sendRawTransaction(srt.hex); - - await db.transaction(async transaction => { - let account = await db.Account.findOne({ - where: { - user_id, - asset: config.liquid.btcasset, - pubkey: null - }, - include: { - model: db.User, - as: "user" - }, - order: [["balance", "DESC"]], - lock: transaction.LOCK.UPDATE, - transaction - }); - - let { user } = account; - - if (Math.round(funded.fee * SATS) > account.balance) { - err( - "amount exceeds balance", - asset_amount, - funded.fee, - account.balance - ); - throw new Error(`Insufficient funds to pay fee of ${funded.fee} BTC`); - } - - await account.decrement( - { balance: Math.round(funded.fee * SATS) }, - { transaction } - ); - await account.reload({ transaction }); - emit(user.username, "account", account); - - let invoice; - let recipient = user; - if (address) { - invoice = await db.Invoice.findOne({ - where: { address }, - include: { - model: db.User, - as: "user" - } - }); - recipient = invoice.user; - } - - if (!invoice) { - invoice = await db.Invoice.create( - { - account_id: account.id, - user_id, - amount: Math.round(params.asset_amount * SATS), - address: asset_address, - currency: user.currency, - rate: store.rates[user.currency], - network: "liquid" - }, - { transaction } - ); - } - - account = await db.Account.create( - { - asset, - contract, - domain, - user_id: invoice.user_id, - ticker, - precision, - name, - balance: 0, - network: "liquid", - pending: Math.round(params.asset_amount * SATS) - }, - { transaction } - ); - - emit(recipient.username, "account", account); - l( - "issued asset", - user.username, - params.asset_amount, - ticker, - name, - account.id - ); - - const asset_payment = await db.Payment.create( - { - account_id: account.id, - user_id: invoice.user_id, - invoice_id: invoice.id, - hash: txid, - amount: Math.round(params.asset_amount * SATS), - received: true, - confirmed: false, - address: asset_address, - network: "liquid" - }, - { transaction } - ); - emit(recipient.username, "payment", asset_payment); - - if (invoice.user_id !== user.id) { - const asset_send = await db.Payment.create( - { - account_id: account.id, - user_id, - hash: txid, - amount: -Math.round(params.asset_amount * SATS), - received: false, - confirmed: false, - address, - network: "liquid" - }, - { transaction } - ); - emit(recipient.username, "payment", asset_send); - } - - store.issuances[txid] = { - user_id, - asset, - asset_amount: params.asset_amount, - asset_payment_id: asset_payment.id - }; - - if (token_amount) { - invoice = null; - recipient = user; - - if (address) { - invoice = await db.Invoice.findOne({ - where: { address }, - include: { - model: db.User, - as: "user" - } - }); - recipient = invoice.user; - } - - if (!invoice) { - invoice = await db.Invoice.create( - { - account_id: account.id, - user_id, - amount: Math.round(params.token_amount * SATS), - address: token_address, - currency: user.currency, - rate: store.rates[user.currency], - network: "liquid" - }, - { transaction } - ); - } - - account = await db.Account.create( - { - asset: token, - user_id: invoice.user_id, - domain, - ticker: `${ticker}REISSUANCETOKEN`, - precision: 8, - name: `${name} Reissuance Token`, - balance: 0, - pending: address ? 0 : Math.round(token_amount * SATS) - }, - { transaction } - ); - emit(recipient.username, "account", account); - - const token_payment = await db.Payment.create( - { - account_id: account.id, - invoice_id: invoice.id, - user_id, - hash: txid, - amount: Math.round(token_amount * SATS), - received: true, - confirmed: false, - address: token_address, - network: "liquid" - }, - { transaction } - ); - emit(recipient.username, "payment", token_payment); - - if (invoice.user_id !== user.id) { - const token_send = await db.Payment.create( - { - account_id: account.id, - invoice_id: invoice.id, - user_id, - hash: txid, - amount: -Math.round(token_amount * SATS), - received: false, - confirmed: false, - address, - network: "liquid" - }, - { transaction } - ); - emit(recipient.username, "payment", token_send); - } - - store.issuances[txid].token = token; - store.issuances[txid].token_amount = token_amount; - store.issuances[txid].token_payment_id = token_payment.id; - } - }); - - res.send(store.issuances[txid] ? store.issuances[txid] : { asset }); - } catch (e) { - err("asset issuance failed", e.message, e.stack); - res.code(500).send(e.message); - } -}); - -app.post("/assets/register", auth, async (req, res) => { - const { asset } = req.body; - const account = await db.Account.findOne({ - where: { - user_id: req.user.id, - asset - } - }); - - l("registering", asset, account.contract); - - try { - const { data: result } = await axios.post( - "https://assets.blockstream.info/", - { - asset_id: asset, - contract: account.contract - } - ); - l("register asset result", req.user.username, result); - res.send(result); - } catch (e) { - err("asset registration failed", e.message); - res.code(500).send(e.message); - } -}); - -app.post("/loadFaucet", auth, async (req, res) => { - return res.code(500).send("Faucet feature temporarily disabled"); - - const { user } = req; - let { asset, amount } = req.body; - amount = parseInt(amount); - - try { - if (amount <= 0) throw new Error("Amount to load cannot be negative"); - - await db.transaction(async transaction => { - let account = await getAccount(config.liquid.btcasset, user, transaction); - - if (account.asset !== config.liquid.btcasset) - throw new Error( - "Faucet has to be funded with bitcoin. Try sending from another wallet." - ); - if (account.balance < amount) throw new Error("Insufficient funds"); - - let fee = 0; - - await account.decrement({ balance: amount }, { transaction }); - await account.reload({ transaction }); - - let a2; - let acc = { - user_id: null, - asset - }; - - a2 = await db.Account.findOne({ - where: acc, - lock: transaction.LOCK.UPDATE, - transaction - }); - - if (a2) { - await a2.increment({ balance: amount }, { transaction }); - await a2.reload({ transaction }); - } else { - let name = asset.substr(0, 6); - let domain; - let ticker = asset.substr(0, 3).toUpperCase(); - let precision = 8; - - const assets = assets; - - if (assets[asset]) { - ({ domain, ticker, precision, name } = assets[asset]); - } else { - const existing = await db.Account.findOne({ - where: { - asset - }, - order: [["id", "ASC"]], - limit: 1, - lock: transaction.LOCK.UPDATE, - transaction - }); - - if (existing) { - ({ domain, ticker, precision, name } = existing); - } - } - - acc = { ...acc, ...{ domain, ticker, precision, name } }; - acc.balance = amount; - acc.pending = 0; - acc.network = "liquid"; - a2 = await db.Account.create(acc, { transaction }); - } - - let params = { - amount: -amount, - account_id: account.id, - user_id: user.id, - rate: store.rates[user.currency], - currency: user.currency, - confirmed: true, - hash: `Loaded Faucet - ${a2.ticker}`, - network: "COINOS" - }; - - let payment = await db.Payment.create(params, { transaction }); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - - l("sent internal", user.username, -payment.amount); - - emit(user.username, "payment", payment); - emit(user.username, "account", account); - emit(user.username, "user", user); - - l("loaded faucet", asset, amount); - res.send({}); - }); - } catch (e) { - err("problem loading faucet", user.username, e.message); - return res.code(500).send(e.message); - } -}); - -app.get("/faucet", auth, async (req, res) => { - let { asset } = req.query; - let faucet = await db.Account.findOne({ - where: { - asset, - user_id: null - } - }); - - if (!faucet) - faucet = { - asset, - balance: 0, - ticker: "Unknown" - }; - - res.send(faucet); -}); diff --git a/routes/bitcoin/broadcast.js b/routes/bitcoin/broadcast.js deleted file mode 100644 index 85c8cf66..00000000 --- a/routes/bitcoin/broadcast.js +++ /dev/null @@ -1,56 +0,0 @@ -import { emit } from "$lib/sockets"; -import store from "$lib/store"; -import { requirePin } from "$lib/utils"; - -export default async (req, res) => { - try { - await requirePin(req); - - const { user } = req; - let hash = await bc.sendRawTransaction(req.body.tx); - - if (user) { - await db.transaction(async transaction => { - let account = await db.Account.findOne({ - where: { - id: user.account_id - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - let payment = await db.Payment.create( - { - ...req.body.payment, - hash, - amount: -req.body.payment.amount, - tip: 0, - account_id: account.id, - user_id: user.id, - rate: store.rates[user.currency], - currency: user.currency, - confirmed: true, - received: false, - network: "bitcoin" - }, - { transaction } - ); - - await account.increment( - { balance: payment.amount - payment.fee }, - { transaction } - ); - await account.reload({ transaction }); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - emit(user.username, "payment", payment); - emit(user.username, "account", payment.account); - res.send(payment); - }); - } else { - res.send(result); - } - } catch (e) { - res.code(500).send(e.message); - } -}; diff --git a/routes/bitcoin/conversionFee.js b/routes/bitcoin/conversionFee.js deleted file mode 100644 index 7e305659..00000000 --- a/routes/bitcoin/conversionFee.js +++ /dev/null @@ -1,12 +0,0 @@ -// Code for the conversion fee -// This is in a separate file so that the same function can be used by ./send and ./receive - -const conversionFeeMultiplier = 0.001; -export const conversionFeeReceiver = "coinosfees"; // account that receives the fees - -/** - * Returns the fee you have to pay for withdrawing 'amount' bitcoin. - */ -export function computeConversionFee(amount) { - return Math.floor(amount * conversionFeeMultiplier); -} diff --git a/routes/bitcoin/fee.js b/routes/bitcoin/fee.js deleted file mode 100644 index 74c26a92..00000000 --- a/routes/bitcoin/fee.js +++ /dev/null @@ -1,73 +0,0 @@ -import config from "$config"; -import db from "$db"; -import bc from "$lib/bitcoin"; -import { err } from "$lib/logging"; -import { emit } from "$lib/sockets"; -import buildTx from "$lib/buildtx"; -import { SATS } from "$lib/utils"; - -export default async (req, res) => { - let { user } = req; - let { address, amount, feeRate, replaceable = true } = req.body; - let tx, fee; - - if (user.account.pubkey) { - try { - let psbt = await buildTx({ address, amount, feeRate, replaceable, user }); - return res.send(psbt); - } catch (e) { - return res.code(500).send(e.message); - } - } - - let invoice = await db.Invoice.findOne({ - where: { address }, - include: { - attributes: ["username"], - model: db.User, - as: "user" - } - }); - - if (invoice) { - let { ismine } = await bc.getAddressInfo(address); - if (ismine) { - emit(user.username, "to", invoice.user); - return res.send({}); - } - } - - try { - amount = parseInt(amount); - - let partial = await bc.createRawTransaction( - [], - { - [address]: (amount / SATS).toFixed(8) - }, - 0, - replaceable - ); - - let params = { - subtractFeeFromOutputs: amount === user.balance ? [0] : [], - replaceable - }; - - if (feeRate) params.feeRate = (feeRate / SATS).toFixed(8); - - tx = await bc.fundRawTransaction(partial, params); - - if (config.bitcoin.walletpass) - await bc.walletPassphrase(config.bitcoin.walletpass, 300); - - let signed = await bc.signRawTransactionWithWallet(tx.hex); - let decoded = await bc.decodeRawTransaction(signed.hex); - feeRate = Math.round((tx.fee * SATS * 1000) / decoded.vsize); - - res.send({ feeRate, tx }); - } catch (e) { - err("bitcoin fee estimation error", e); - return res.code(500).send(e.message); - } -}; diff --git a/routes/bitcoin/generate.js b/routes/bitcoin/generate.js deleted file mode 100644 index 6285492d..00000000 --- a/routes/bitcoin/generate.js +++ /dev/null @@ -1,5 +0,0 @@ -export default async (req, res) => { - let address = await bc.getNewAddress(); - await bc.generateToAddress(1, address); - res.send({}); -}; diff --git a/routes/bitcoin/index.js b/routes/bitcoin/index.js deleted file mode 100644 index 58b0d193..00000000 --- a/routes/bitcoin/index.js +++ /dev/null @@ -1,13 +0,0 @@ -import broadcast from "./broadcast"; -import generate from "./generate"; -import sweep from "./sweep"; -import fee from "./fee"; -import send from "./send"; - -export default { - broadcast, - generate, - sweep, - fee, - send -}; diff --git a/routes/bitcoin/receive.js b/routes/bitcoin/receive.js deleted file mode 100644 index 10193ceb..00000000 --- a/routes/bitcoin/receive.js +++ /dev/null @@ -1,281 +0,0 @@ -import config from "$config"; -import db from "$db"; -import bc from "$lib/bitcoin"; -import { emit } from "$lib/sockets"; -import store from "$lib/store"; -import { notify } from "$lib/notifications"; -import { callWebhook } from "$lib/webhooks"; -import reverse from "buffer-reverse"; -import zmq from "zeromq/v5-compat"; -import { Op } from "@sequelize/core"; -import { fromBase58 } from "bip32"; -import bitcoin from "bitcoinjs-lib"; -import { sendLiquid } from "$routes/liquid/send"; -import { computeConversionFee } from "./conversionFee"; -import { l, warn, err } from "$lib/logging"; - -const zmqRawBlock = zmq.socket("sub"); -zmqRawBlock.connect(config.bitcoin.zmqrawblock); -zmqRawBlock.subscribe("rawblock"); - -const zmqRawTx = zmq.socket("sub"); -zmqRawTx.connect(config.bitcoin.zmqrawtx); -zmqRawTx.subscribe("rawtx"); - -const network = - bitcoin.networks[ - config.bitcoin.network === "mainnet" ? "bitcoin" : config.bitcoin.network - ]; - -const queue = {}; -const seen = []; - -console.warn = () => {}; - -zmqRawTx.on("message", async (topic, message, sequence) => { - try { - const hex = message.toString("hex"); - let tx = bitcoin.Transaction.fromHex(message); - - let hash = reverse(tx.getHash()).toString("hex"); - - if (seen.includes(hash)) return; - seen.push(hash); - if (seen.length > 5000) seen.shift(); - - Promise.all( - tx.outs.map(async o => { - try { - const { value } = o; - - let address; - try { - address = bitcoin.address.fromOutputScript(o.script, network); - } catch (e) { - return; - } - - if ( - Object.keys(store.addresses).includes(address) && - !store.change.includes(address) - ) { - await db.transaction(async transaction => { - let user = await db.User.findOne({ - where: { - username: store.addresses[address] - }, - transaction - }); - - let invoice = await db.Invoice.findOne({ - where: { - address, - user_id: user.id, - network: "bitcoin" - }, - order: [["id", "DESC"]], - include: { - model: db.Account, - as: "account" - }, - transaction - }); - - if (!invoice) return; - - const currency = invoice ? invoice.currency : user.currency; - const rate = invoice ? invoice.rate : store.rates[user.currency]; - const tip = invoice ? invoice.tip : 0; - const memo = invoice ? invoice.memo : ""; - - let confirmed = false; - - let { account } = invoice; - - if (account.asset !== config.liquid.btcasset) { - account = await db.Account.findOne({ - where: { - user_id: user.id, - asset: config.liquid.btcasset, - pubkey: null - }, - transaction - }); - } - - await invoice.increment({ pending: value }, { transaction }); - await invoice.reload({ transaction }); - - await account.increment({ pending: value }, { transaction }); - await account.reload({ transaction }); - - if (config.bitcoin.walletpass) - await bc.walletPassphrase(config.bitcoin.walletpass, 300); - - let totalOutputs = tx.outs.reduce((a, b) => a + b.value, 0); - let totalInputs = 0; - for (let i = 0; i < tx.ins.length; i++) { - let { hash, index } = tx.ins[i]; - hash = reverse(hash).toString("hex"); - let hex = await bc.getRawTransaction(hash); - let inputTx = bitcoin.Transaction.fromHex(hex); - totalInputs += inputTx.outs[index].value; - } - let fee = totalInputs - totalOutputs; - - let payment = await db.Payment.create( - { - account_id: account.id, - user_id: user.id, - hash, - fee, - memo, - amount: value - tip, - currency, - rate, - received: true, - tip, - confirmed, - address, - network: "bitcoin", - invoice_id: invoice.id - }, - { transaction } - ); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - payment.invoice = invoice.get({ plain: true }); - - emit(user.username, "account", account); - emit(user.username, "payment", payment); - l("bitcoin detected", user.username, value); - notify(user, `${value} SAT payment detected`); - callWebhook(invoice, payment); - }); - } - } catch (e) { - console.log("problem parsing bitcoin tx", e); - } - }) - ); - } catch (e) { - console.log("problem receiving bitcoin tx", e); - } -}); - -zmqRawBlock.on("message", async (topic, message, sequence) => { - try { - const payments = await db.Payment.findAll({ - where: { confirmed: false } - }); - - let block = bitcoin.Block.fromHex(message.toString("hex")); - block.transactions.map(tx => { - let hash = reverse(tx.getHash()).toString("hex"); - if (payments.find(p => p.hash === hash)) queue[hash] = 1; - }); - } catch (e) { - console.log(e); - } -}); - -setInterval(async () => { - try { - const arr = Object.keys(queue); - for (let i = 0; i < arr.length; i++) { - const hash = arr[i]; - - let account, address, invoice, user, total; - await db.transaction(async transaction => { - let payments = await db.Payment.findAll({ - where: { hash, confirmed: 0, received: 1 }, - include: [ - { - model: db.Account, - as: "account" - }, - { - model: db.Invoice, - as: "invoice" - }, - { - model: db.User, - as: "user" - } - ], - lock: transaction.LOCK.UPDATE, - transaction - }); - - for (let i = 0; i < payments.length; i++) { - let p = payments[i]; - if (p && p.address) address = p.address; - if (p && p.user) user = p.user; - - if (p && p.account) { - ({ account, invoice } = p); - total = p.amount + p.tip; - - p.confirmed = 1; - await account.save({ transaction }); - - await invoice.increment({ received: total }, { transaction }); - await invoice.decrement( - { pending: Math.min(invoice.pending, total) }, - { transaction } - ); - - await account.increment({ balance: total }, { transaction }); - await account.decrement( - { pending: Math.min(account.pending, total) }, - { transaction } - ); - // get the # of fee credits you would need to pay off this amount of bitcoin - await account.increment( - { btc_credits: computeConversionFee(total) }, - { transaction } - ); - await account.reload({ transaction }); - await invoice.reload({ transaction }); - await p.save({ transaction }); - - p = p.get({ plain: true }); - p.account = account.get({ plain: true }); - p.invoice = invoice.get({ plain: true }); - - emit(user.username, "account", account); - emit(user.username, "payment", p); - l("bitcoin confirmed", user.username, p.amount, p.tip); - notify(user, `${total} SAT payment confirmed`); - callWebhook(p.invoice, p); - - let c = store.convert[address]; - if (address && c) { - l( - "bitcoin detected to conversion address", - address, - c.address, - user.username - ); - user.account = account; - - sendLiquid({ - address: c.address, - amount: total - 100, - user, - limit: total - }); - } - } else { - warn("couldn't find bitcoin payment", hash); - } - } - - delete queue[hash]; - }); - } - } catch (e) { - err("problem processing queued bitcoin transaction", e.message, e.stack); - } -}, 1000); diff --git a/routes/bitcoin/send.js b/routes/bitcoin/send.js deleted file mode 100644 index c52592ae..00000000 --- a/routes/bitcoin/send.js +++ /dev/null @@ -1,193 +0,0 @@ -import bc from "$lib/bitcoin"; -import { emit } from "$lib/sockets"; -import db from "$db"; -import { toSats } from "$lib/utils"; -import store from "$lib/store"; -import config from "$config"; -import { l, warn, err } from "$lib/logging"; -import { requirePin } from "$lib/utils"; - -const btc = config.liquid.btcasset; -import { - computeConversionFee, - conversionFeeReceiver, -} from "./conversionFee.js"; - -export default async (req, res) => { - try { - await requirePin(req); - - let { user } = req; - let { address, memo, tx } = req.body; - let { hex } = tx; - - let fee = toSats(tx.fee); - if (fee < 0) throw new Error("fee cannot be negative"); - - const isChange = async ({ address }) => - (await bc.getAddressInfo(address)).ismine && - !Object.keys(store.addresses).includes(address); - - tx = await bc.decodeRawTransaction(hex); - - let total = 0; - let change = 0; - - for (let i = 0; i < tx.vout.length; i++) { - let o = tx.vout[i]; - total += toSats(o.value); - - if (await isChange(o.scriptPubKey)) { - change += toSats(o.value); - } - } - - total = total - change + fee; - let amount = total - fee; - - // get conversion fee - // 'total' refers to the total before the conversion fee - // (i.e. the total bitcoin that leaves this server) - let conversionFee = computeConversionFee(amount); - - // withdraw bitcoin - await db.transaction(async (transaction) => { - let { account } = user; - if (account.asset !== btc) { - account = await db.Account.findOne({ - where: { - user_id: user.id, - asset: btc, - pubkey: null, - }, - lock: transaction.LOCK.UPDATE, - transaction, - }); - } - - // use user's credits to reduce fee, if available - let conversionFeeDeduction = Math.min(account.btc_credits, conversionFee); - if (conversionFeeDeduction) { - await account.decrement( - { btc_credits: conversionFeeDeduction }, - { transaction } - ); - await account.reload({ transaction }); - conversionFee -= conversionFeeDeduction; - } - - l( - "attempting to send bitcoin", - user.username, - account.id, - address, - total, - conversionFee, - conversionFeeDeduction - ); - - if (total > account.balance) { - err("amount exceeds balance", amount, fee, account.balance); - throw new Error("low"); - } else if (total + conversionFee > account.balance) { - err( - "total (after conversion fee) exceeds balance", - amount, - fee, - account.balance - ); - throw new Error("low"); - } - - await account.decrement( - { balance: total + conversionFee }, - { transaction } - ); - await account.reload({ transaction }); - - 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: "Bitcoin 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; - } - - // record the external bitcoin transaction - let params = { - amount: -amount, - fee, - memo, - account_id: account.id, - user_id: user.id, - rate: store.rates[user.currency], - currency: user.currency, - address, - confirmed: true, - received: false, - network: "bitcoin", - fee_payment_id: fee_payment_id, - }; - - if (config.bitcoin.walletpass) - await bc.walletPassphrase(config.bitcoin.walletpass, 300); - - hex = (await bc.signRawTransactionWithWallet(hex)).hex; - params.hash = await bc.sendRawTransaction(hex); - - let payment = await db.Payment.create(params, { transaction }); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - payment.fee_payment = fee_payment && fee_payment.get({ plain: true }); - - emit(user.username, "payment", payment); - res.send(payment); - - store.payments.push(params.hash); - l("sent bitcoin", user.username, total); - }); - } catch (e) { - if (e.message.includes("Insufficient")) - e.message = - "Our server has insufficient funds, try again later"; - - if (e.message === "low") e.message = "Insufficient funds in account" - - err("error sending bitcoin", e.message); - console.log(e); - return res.code(500).send(e.message); - } -}; diff --git a/routes/bitcoin/sweep.js b/routes/bitcoin/sweep.js deleted file mode 100644 index 49fb1f04..00000000 --- a/routes/bitcoin/sweep.js +++ /dev/null @@ -1,87 +0,0 @@ -import config from "$config"; -import { prod } from "$lib/utils"; -import axios from "axios"; -import bitcoin from "bitcoinjs-lib"; -import coinselect from "coinselect"; -import split from "coinselect/split"; - -const api = prod ? "https://blockstream.info/api" : config.bitcoin.electrs; -const SATS = 100000000; -const network = prod - ? bitcoin.networks["bitcoin"] - : bitcoin.networks["regtest"]; - -export default async (req, res) => { - let { address: from, amount, feeRate, target } = req.body; - let utxos; - if (!feeRate) feeRate = (await bc.estimateSmartFee(6)).feerate * SATS; - feeRate = Math.round(feeRate / 1000); - l("sweeping", req.user.username, from, amount, feeRate); - - try { - let { - data: { - chain_stats: { funded_txo_sum: funded, spent_txo_sum: spent } - } - } = await axios.get(`${api}/address/${from}`); - let balance = funded - spent; - console.log("boom"); - - try { - console.log("fetching utxo"); - ({ data: utxos } = await axios.get(`${api}/address/${from}/utxo`)); - console.log("got utxos", utxos); - } catch (e) { - err("problem fetching utxos", e.message); - } - - let targets = [ - { - address: target, - value: amount - } - ]; - - let { inputs, outputs, fee } = coinselect(utxos, targets, feeRate); - - if (balance === amount) { - delete targets[0].value; - ({ inputs, outputs, fee } = split(utxos, targets, feeRate)); - l("split", inputs, outputs, fee); - } - - if (!inputs || !outputs) - return res - .code(500) - .send("Unable to construct sweep transaction, try a lower fee rate?"); - - let psbt = new bitcoin.Psbt({ network }); - let total = 0; - - outputs.map(({ address, value }) => { - total += value; - psbt = psbt.addOutput({ - address: address || from, - value - }); - }); - - for (let i = 0; i < inputs.length; i++) { - let { txid, vout } = inputs[i]; - let nonWitnessUtxo = Buffer.from(await bc.getRawTransaction(txid), "hex"); - psbt = psbt.addInput({ - hash: txid, - index: vout, - nonWitnessUtxo - }); - } - - feeRate = Math.round(feeRate * 1000); - psbt = psbt.toBase64(); - - if (balance !== amount) total = 0; - res.send({ feeRate, psbt, total }); - } catch (e) { - err("problem getting address stats", from, e.message); - } -}; diff --git a/routes/email.js b/routes/email.js new file mode 100644 index 00000000..529307e6 --- /dev/null +++ b/routes/email.js @@ -0,0 +1,24 @@ +import config from "$config"; + +export default { + async send({ body }, res) { + try { + let postmark = await import("postmark"); + let client = new postmark.ServerClient(config.postmark); + + await client.sendEmail({ + From: "[email protected]", + To: "[email protected]", + Subject: body.subject || "Email Signup", + HtmlBody: JSON.stringify(body), + TextBody: JSON.stringify(body), + MessageStream: "outbound" + }); + + res.send({ ok: true }); + } catch (e) { + console.log("problem sending email", e); + res.code(500).send(e.message); + } + } +}; diff --git a/routes/funding.js b/routes/funding.js deleted file mode 100644 index e65a704e..00000000 --- a/routes/funding.js +++ /dev/null @@ -1,96 +0,0 @@ -import { emit } from "$lib/sockets"; -import app from "$app"; -import config from "$config"; -import mailgunFactory from "mailgun-js"; -const mailgun = mailgunFactory(config.mailgun); - -app.post("/id", auth, upload.single("id"), async (req, res) => { - const { user } = req; - if (user.verified === "proof") user.verified = "pending"; - else user.verified = "id"; - await user.save(); - emit(user.username, "user", user); - - let data = { - subject: "KYC Documents", - text: `ID uploaded for ${user.username}`, - from: "[email protected]", - to: "[email protected]" - }; - - mailgun.messages().send(data); - l("id uploaded", user.username); - - res.send({}); -}); - -app.post("/proof", auth, upload.single("proof"), async (req, res) => { - const { user } = req; - if (user.verified === "id") user.verified = "pending"; - else user.verified = "proof"; - await user.save(); - emit(user.username, "user", user); - l("id proof uploaded", user.username); - - res.send({}); -}); - -app.post("/funding", auth, async (req, res) => { - try { - let { id, amount, code } = req.body; - - if (!code) { - code = ""; - let d = "ABCDEFGHIJKLMNPQRSTUVWXYZ23456789"; - let l = d.length; - for (let i = 0; i < 8; i++) { - code += d.charAt(Math.floor(Math.random() * l)); - } - } - - let params = { - user_id: req.user.id, - amount, - code - }; - - let deposit; - if (id) { - if (!parseFloat(amount) || parseFloat(amount) < 0) - throw new Error("Invalid amount"); - deposit = await db.Deposit.findOne({ where: { id } }); - deposit.update(params); - await deposit.save(); - } else { - deposit = await db.Deposit.create(params); - } - - res.send(deposit); - } catch (e) { - err("funding error", e.message); - res.code(500).send("Funding request failed"); - } -}); - -app.post("/withdrawal", auth, async (req, res) => { - try { - const { account, amount, email, institution, transit, notes } = req.body; - if (!parseFloat(amount) || parseFloat(amount) < 0) - throw new Error("Invalid amount"); - - await db.Withdrawal.create({ - user_id: req.user.id, - account, - amount, - email, - institution, - transit, - notes - }); - - res.send({}); - } catch (e) { - err("withdrawaerr", e.message); - res.code(500).send("Withdrawal request failed"); - } -}); diff --git a/routes/info.js b/routes/info.js index eb42ca35..017e473a 100644 --- a/routes/info.js +++ b/routes/info.js @@ -1,90 +1,32 @@ -import app from "$app"; -import db from "$db"; -import config from "$config"; -import store from "$lib/store"; - -import bc from "$lib/bitcoin"; -import lq from "$lib/liquid"; +import db from "$lib/db"; import ln from "$lib/ln"; -import sequelize from "@sequelize/core"; -import { SATS } from "$lib/utils"; - -const { Op } = sequelize; - -/** - * @api {get} /info Request server information - * @apiName GetInfo - * @apiGroup Info - * - * @apiSuccess {Object} fx Exchange rates relative to USD - fx.CUR is the value of one CUR in USD. - * @apiSuccess {String[]} networks Array of supported networks; possible values are "bitcoin", "liquid" and "lightning". - * @apiSuccess {String} clientVersion the current git commit of the ui - */ -app.get("/info", async (req, res, next) => { - const { clientVersion } = config; - - const info = { - fx: store.fx, - nodes: store.networks, - clientVersion - }; - - res.send(info); -}); +export default { + async balances(req, res) { + let lnchannel; + let lnwallet; -app.get("/balances", async (req, res, next) => { - try { - const accounts = await db.Account.findAll({ - where: { - pubkey: null - }, - attributes: [ - "asset", - [sequelize.fn("sum", sequelize.col("balance")), "total"] - ], - group: ["asset"] + const stream = db.scanStream({ + match: "balance:*", + count: 100 }); - let lnchannel; - let lnwallet; + let b = 0; + for await (let keys of stream) { + for (let k of keys) b += parseInt(await db.get(k)); + } + console.log(b); const funds = await ln.listfunds(); lnchannel = parseInt(funds.channels.reduce((a, b) => a + b.channel_sat, 0)); lnwallet = parseInt(funds.outputs.reduce((a, b) => a + b.value, 0)); - let bitcoin, bitcoind; - if (config.bitcoin) { - bitcoin = parseInt((await bc.getBalance()) * SATS); - bitcoind = await bc.getNetworkInfo(); - } - - let assets, liquid, elementsd; - if (config.liquid) { - assets = await lq.getBalance(); - liquid = parseInt(assets.bitcoin * SATS); - elementsd = await lq.getNetworkInfo(); - } - const info = { - bitcoind, - elementsd, - accounts, - assets, - bitcoin, - liquid, lnchannel, - lnwallet + lnwallet, + total: lnchannel + lnwallet }; - info.total = - parseInt(bitcoin) + - parseInt(liquid) + - parseInt(lnchannel) + - parseInt(lnwallet); - res.send(info); - } catch (e) { - console.log("problem getting balances", e); } -}); +}; diff --git a/routes/invoices.js b/routes/invoices.js index 7fa82afb..d856ef6b 100644 --- a/routes/invoices.js +++ b/routes/invoices.js @@ -1,209 +1,39 @@ -import app from "$app"; -import db from "$db"; -import store from "$lib/store"; -import { optionalAuth } from "$lib/passport"; -import { Op } from "@sequelize/core"; -import axios from "axios"; -import { err, l } from "$lib/logging"; -import bc from "$lib/bitcoin"; -import lq from "$lib/liquid"; -import { SATS, bip21, deriveAddress, derivePayRequest } from "$lib/utils"; -import { emit } from "$lib/sockets"; +import { g, s } from "$lib/db"; +import { generate } from "$lib/invoices"; +import got from "got"; +import config from "$config"; -app.get("/invoice", async (req, res, next) => { - try { - const invoice = await db.Invoice.findOne({ - where: { - uuid: req.query.uuid - }, - include: [ - { - model: db.User, - as: "user", - attributes: ["username", "currency", "profile"] - }, - { - model: db.Request, - as: "request" - } - ] - }); - - res.send(invoice); - } catch (e) { - err("invoice not found", e); - res.code(500).send("Invoice not found"); - } -}); - -app.get("/invoice/:text", async (req, res, next) => { - try { - let { text } = req.params; - let where = - text.split("-").length > 4 - ? { uuid: text } - : text.startsWith("ln") - ? { text } - : { - [Op.or]: [{ unconfidential: text }, { address: text }] - }; - - const invoice = await db.Invoice.findOne({ - where, - include: { - model: db.User, - as: "user", - attributes: ["username", "currency", "profile"] - } - }); - - res.send(invoice); - } catch (e) { - err("invoice not found", e); - res.code(500).send("Invoice not found"); - } -}); - -app.post("/invoice", optionalAuth, async (req, res, next) => { - try { - let { type = "bech32", liquidAddress, id, invoice, user, tx } = req.body; - let { - blindkey, - currency, - tip, - amount, - rate, - network, - request_id - } = invoice; - let address, unconfidential, text; - - if (amount < 0) throw new Error("amount out of range"); - if (tip > 5 * amount || tip < 0) throw new Error("tip amount out of range"); - - if (!user) ({ user } = req); - else { - user = await db.User.findOne({ - where: { - username: user.username - }, - include: { - model: db.Account, - as: "account" - } - }); - } - if (!user) throw new Error("user not provided"); - if (!currency) currency = user.currency; - if (!rate) rate = store.rates[currency]; - if (amount < 0) throw new Error("invalid amount"); +export default { + async get({ params: { hash } }, res) { + let invoice = await g(`invoice:${hash}`); - if (user.account.pubkey) { - let { address, confidentialAddress } = await deriveAddress( - user.account, - type - ); - if (confidentialAddress) { - address = confidentialAddress; - unconfidential = address; - } else { - address = address; - } - } else if (network !== "lightning") { - address = await { bitcoin: bc, liquid: lq }[network].getNewAddress(); - if (network === "liquid") - ({ unconfidential } = await lq.getAddressInfo(address)); - } - - invoice = { - ...invoice, - account_id: user.account.id, - address, - amount, - currency, - rate, - tip, - unconfidential, - user_id: user.id - }; - - if (network === "lightning") { - invoice.text = await derivePayRequest(invoice); + if (invoice) { + invoice.user = await g(`user:${invoice.uid}`); } else { - invoice.text = bip21(invoice, user.account); + invoice = await got(`${config.classic}/invoice/${hash}`).json(); + invoice.id = invoice.uuid; + invoice.classic = true; + invoice.user.id = invoice.user.uuid; + invoice.user.username += "@classic"; } - if (liquidAddress) { - if (network === "lightning") { - l("conversion request for", liquidAddress, invoice.text); - store.convert[invoice.text] = { address: liquidAddress, tx }; - } else if (network === "liquid") { - l("conversion request for", liquidAddress, invoice.unconfidential); - store.convert[invoice.unconfidential] = { address: liquidAddress, tx }; - } else { - l("conversion request for", liquidAddress, invoice.address); - store.convert[invoice.address] = { address: liquidAddress, tx }; - } - } - - l( - "creating invoice", - user.username, - invoice.network, - invoice.amount, - invoice.tip, - invoice.currency, - invoice.text && `${invoice.text.substr(0, 8)}..${invoice.text.substr(-6)}` - ); - - if (!invoice.tip) invoice.tip = 0; - - invoice = await db.Invoice.create(invoice); - store.addresses[invoice.address] = user.username; - if (invoice.unconfidential) { - store.addresses[invoice.unconfidential] = user.username; - if (blindkey) await lq.importBlindingKey(invoice.address, blindkey); - } + res.send(invoice); + }, - if (request_id) { - let request = await db.Request.findOne({ - where: { id: request_id }, + async create({ body: { invoice, user }, user: sender }, res) { + res.send(await generate({ invoice, user, sender })); + }, - include: { - model: db.User, - as: "requester", - attributes: ["username"] + async classic({ params: { username } }, res) { + let invoice = await got + .post(`${config.classic}/invoice`, { + json: { + invoice: { amount: 0, network: "lightning" }, + user: { username } } - }); - - if (request) { - let existing = request.invoice_id; - request.invoice_id = invoice.id; - await request.save(); - - if (!existing) - emit(request.requester.username, "invoice", { - amount: invoice.amount, - currency: invoice.currency, - rate: invoice.rate, - request_id, - uuid: invoice.uuid, - user: { username: user.username, profile: user.profile } - }); - } - } + }) + .json(); res.send(invoice); - } catch (e) { - err(e.message, e.stack); - res.code(500).send(`Problem during invoice creation: ${e.message}`); - } -}); - -app.post( - "/:username/:network/invoice", - optionalAuth, - async (req, res, next) => { - let { network, username } = req.params; } -); +}; diff --git a/routes/lightning/channel.js b/routes/lightning/channel.js deleted file mode 100644 index d5d5b7b2..00000000 --- a/routes/lightning/channel.js +++ /dev/null @@ -1,29 +0,0 @@ -import axios from "axios"; -import ln from "$lib/ln"; - -export default async (req, res) => { - const { params } = req.body; - const [public_key, socket] = params.uri.split("@"); - - l("connecting to peer", req.user.username, public_key, socket); - let result; - - const { callback, k1 } = params; - let remoteid; - try { - remoteid = (await ln.getinfo()).id; - } catch (e) { - err("problem getting lightning node info", e.message); - return res.code(500).send(e.message); - } - - let url = `${callback}?k1=${k1}&remoteid=${remoteid}&private=0`; - - try { - let response = await axios.get(url); - res.send(response.data); - } catch (e) { - err("problem sending channel request", e.message); - return res.code(500).send({ status: "ERROR", reason: e.message }); - } -}; diff --git a/routes/lightning/channelRequest.js b/routes/lightning/channelRequest.js deleted file mode 100644 index 74e4cbba..00000000 --- a/routes/lightning/channelRequest.js +++ /dev/null @@ -1,24 +0,0 @@ -import { lnurlServer } from "$routes/lnurl"; -import { err } from "$lib/logging"; - -export const channelRequest = async (req, res) => { - let { localAmt, pushAmt } = req.body; - - pushAmt = 0; - - try { - if (localAmt < 20000) - throw new Error("amount must be greater than 20000 SAT"); - if (localAmt > 1000000) throw new Error("amount must be <= 1000000"); - - const result = await lnurlServer.generateNewUrl("channelRequest", { - localAmt, - pushAmt - }); - - res.send(result); - } catch (e) { - err("problem generating channel request", e.message); - res.code(500).send(e.message); - } -}; diff --git a/routes/lightning/conversionFee.js b/routes/lightning/conversionFee.js deleted file mode 100644 index f6cf434c..00000000 --- a/routes/lightning/conversionFee.js +++ /dev/null @@ -1,12 +0,0 @@ -// Code for the conversion fee -// This is in a separate file so that the same function can be used by ./send and ./receive - -const conversionFeeMultiplier = 0.001; -export const conversionFeeReceiver = "coinosfees"; // account that receives the fees - -/** - * Returns the fee you have to pay for withdrawing 'amount' lightning BTC. - */ -export function computeConversionFee(amount) { - return Math.floor(amount * conversionFeeMultiplier); -} diff --git a/routes/lightning/index.js b/routes/lightning/index.js deleted file mode 100644 index b4611ef5..00000000 --- a/routes/lightning/index.js +++ /dev/null @@ -1,13 +0,0 @@ -import channel from "./channel"; -import invoice from "./invoice"; -import query from "./query"; -import send from "./send"; -import parse from "./parse"; - -export default { - channel, - invoice, - query, - send, - parse -}; diff --git a/routes/lightning/invoice.js b/routes/lightning/invoice.js deleted file mode 100644 index ae974120..00000000 --- a/routes/lightning/invoice.js +++ /dev/null @@ -1,25 +0,0 @@ -import config from "$config"; -import { err } from "$lib/logging"; -import ln from "$lib/ln"; -import { v4 } from "uuid"; - -export default async (req, res) => { - let { amount, memo, tip } = req.body; - if (!tip) tip = 0; - let value = amount + tip; - - try { - if (!memo) memo = "coinos"; - const invoice = await ln.invoice( - value ? `${value}sat` : "any", - v4(), - memo, - 360 - ); - res.send({ text: invoice.bolt11 }); - } catch (e) { - console.log(e); - err("problem creating invoice", e.message); - res.code(500).send(e.message); - } -}; diff --git a/routes/lightning/parse.js b/routes/lightning/parse.js deleted file mode 100644 index 1b77019a..00000000 --- a/routes/lightning/parse.js +++ /dev/null @@ -1,19 +0,0 @@ -import ln from "$lib/ln"; -import store from "$lib/store"; - -export default async (req, res) => { - let { payreq } = req.body; - let hour = 1000 * 60 * 60; - let { last } = store.nodes; - let { nodes } = store; - - if (!last || last > Date.now() - hour) ({ nodes } = await ln.listnodes()); - store.nodes = nodes; - - let twoWeeksAgo = new Date(new Date().setDate(new Date().getDate() - 14)); - let { msatoshi, payee } = await ln.decodepay(payreq); - let node = nodes.find((n) => n.nodeid === payee); - let alias = node ? node.alias : payee.substr(0, 12); - - res.send({ alias, amount: Math.round(msatoshi / 1000) }); -}; diff --git a/routes/lightning/query.js b/routes/lightning/query.js deleted file mode 100644 index 1c217a08..00000000 --- a/routes/lightning/query.js +++ /dev/null @@ -1,20 +0,0 @@ -import db from "$db"; -import { emit } from "$lib/sockets"; - -export default async (req, res) => { - const { payreq } = req.body; - - const invoice = await db.Invoice.findOne({ - include: { - attributes: ["username"], - model: db.User, - as: "user" - }, - where: { - text: payreq - } - }); - - if (invoice) emit(req.user.username, "to", invoice.user); - res.send({}); -}; diff --git a/routes/lightning/receive.js b/routes/lightning/receive.js deleted file mode 100644 index ef973acc..00000000 --- a/routes/lightning/receive.js +++ /dev/null @@ -1,162 +0,0 @@ -import db from "$db"; -import store from "$lib/store"; -import { emit } from "$lib/sockets"; -import config from "$config"; -import { notify } from "$lib/notifications"; -import { callWebhook } from "$lib/webhooks"; -import { computeConversionFee } from "./conversionFee"; -import { sendLiquid } from "$routes/liquid/send"; -import ln from "$lib/ln"; -import { l, err, warn } from "$lib/logging"; - -const handlePayment = async msg => { - try { - l("incoming lightning payment", msg.received); - - if (!msg.secret) return; - let account, total, user; - - const invoice = await db.Invoice.findOne({ - where: { - text: msg.request - } - }); - - if (!invoice) - return warn("received lightning with no invoice", msg.request); - - await db.transaction(async transaction => { - const { text: hash, currency, memo, rate, tip, user_id } = invoice; - const amount = parseInt(msg.received) - tip; - if (amount < 0) throw new Error("amount out of range"); - - account = await db.Account.findOne({ - where: { - user_id, - asset: config.liquid.btcasset, - pubkey: null - }, - include: { - model: db.User, - as: "user" - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - ({ user } = account); - - let preimage = msg.secret; - - let payment = await db.Payment.create( - { - account_id: account.id, - user_id, - hash, - memo, - amount, - currency, - preimage, - rate, - received: true, - confirmed: true, - network: "lightning", - tip, - invoice_id: invoice.id - }, - { transaction } - ); - - total = amount + tip; - - invoice.status = "paid"; - await invoice.save({ transaction }); - - await invoice.increment({ received: total }, { transaction }); - await account.increment({ balance: total }, { transaction }); - // get the # of fee credits you would need to pay off this amount of bitcoin - await account.increment( - { lightning_credits: computeConversionFee(total) }, - { transaction } - ); - await account.reload({ transaction }); - await invoice.reload({ transaction }); - await payment.save({ transaction }); - store.payments.push(msg.request); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - payment.invoice = invoice.get({ plain: true }); - - callWebhook(invoice, payment); - - emit(user.username, "payment", payment); - emit(user.username, "account", payment.account); - notify(user, `Received ${total} SAT`); - - l( - "lightning payment received", - user.username, - payment.amount, - payment.tip - ); - }); - - let c = store.convert[msg.request]; - if (msg.request && c) { - l( - "lightning detected for conversion request", - msg.request, - c.address, - user.username - ); - - user.account = account; - - try { - sendLiquid({ - address: c.address, - amount: total - 100, - user, - limit: total - }); - } catch (e) { - err("problem sending liquid payment", e.message, e.stack); - } - } - } catch (e) { - console.log(e); - err("problem receiving lightning payment", e.message); - } -}; - -const poll = async ln => { - try { - const wait = async i => { - const { - bolt11: request, - pay_index, - status, - msatoshi_received, - payment_preimage: secret - } = await ln.waitanyinvoice(i); - - let settled = status === "paid"; - let received = parseInt(msatoshi_received / 1000); - - await handlePayment({ - request, - received, - secret - }); - await wait(pay_index); - }; - - const { invoices } = await ln.listinvoices(); - await wait(Math.max(...invoices.map(i => i.pay_index).filter(n => n))); - } catch (e) { - err("problem waiting for c-lightning invoice", e.message); - } -}; - -poll(ln); diff --git a/routes/lightning/send.js b/routes/lightning/send.js deleted file mode 100644 index b41d5ed0..00000000 --- a/routes/lightning/send.js +++ /dev/null @@ -1,17 +0,0 @@ -import { err } from "$lib/logging"; -import send from "$lib/send"; -import { requirePin } from "$lib/utils"; - -export default async (req, res) => { - let { amount, route, memo, payreq } = req.body; - let { user } = req; - - try { - await requirePin(req); - res.send(await send(amount, memo, payreq, user)); - } catch (e) { - console.log(e); - err("problem sending lightning payment", user.username, payreq, e.message); - res.code(500).send(e.message); - } -}; diff --git a/routes/liquid/broadcast.js b/routes/liquid/broadcast.js deleted file mode 100644 index 820c0be1..00000000 --- a/routes/liquid/broadcast.js +++ /dev/null @@ -1,58 +0,0 @@ -import { emit } from "$lib/sockets"; -import store from "$lib/store"; -import { requirePin } from "$lib/utils"; - -export default async (req, res) => { - try { - await requirePin(req); - - const { user } = req; - let hash = await lq.sendRawTransaction(req.body.tx); - - if (user) { - await db.transaction(async transaction => { - let account = await db.Account.findOne({ - where: { - id: user.account_id - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - let payment = await db.Payment.create( - { - ...req.body.payment, - hash, - amount: -req.body.payment.amount, - tip: 0, - account_id: account.id, - user_id: user.id, - rate: store.rates[user.currency], - currency: user.currency, - confirmed: true, - received: false, - network: "liquid" - }, - { transaction } - ); - - await account.increment( - { balance: payment.amount - payment.fee }, - { transaction } - ); - await account.reload({ transaction }); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - emit(user.username, "payment", payment); - emit(user.username, "account", payment.account); - res.send(payment); - }); - } else { - res.send(hash); - } - } catch (e) { - err("problem broadcasting liquid transaction", e); - res.code(500).send(e.message); - } -}; diff --git a/routes/liquid/conversionFee.js b/routes/liquid/conversionFee.js deleted file mode 100644 index a25c2e7d..00000000 --- a/routes/liquid/conversionFee.js +++ /dev/null @@ -1,12 +0,0 @@ -// Code for the conversion fee -// This is in a separate file so that the same function can be used by ./send and ./receive - -const conversionFeeMultiplier = 0.001; -export const conversionFeeReceiver = "coinosfees"; // account that receives the fees - -/** - * Returns the fee you have to pay for withdrawing 'amount' L-BTC. - */ -export function computeConversionFee(amount) { - return Math.floor(amount * conversionFeeMultiplier); -} diff --git a/routes/liquid/fee.js b/routes/liquid/fee.js deleted file mode 100644 index ae6e4393..00000000 --- a/routes/liquid/fee.js +++ /dev/null @@ -1,89 +0,0 @@ -import config from "$config"; -import db from "$db"; -import { emit } from "$lib/sockets"; -import buildTx from "$lib/buildliquidtx"; -import { err, l } from "$lib/logging"; -import { SATS } from "$lib/utils"; -import lq from "$lib/liquid"; - -export const liquidTx = async ({ address, asset, amount, feeRate, replaceable, user }) => { - let tx, fee; - - if (user.account.pubkey) { - let psbt = await buildTx({ - address, - asset, - amount, - feeRate, - replaceable, - user - }); - return psbt; - } - - let invoice = await db.Invoice.findOne({ - where: { address }, - include: { - attributes: ["username"], - model: db.User, - as: "user" - } - }); - - if (invoice) { - let { ismine } = await lq.getAddressInfo(address); - if (ismine) { - emit(user.username, "to", invoice.user); - return; - } - } - - if (config.liquid.walletpass) - await lq.walletPassphrase(config.liquid.walletpass, 300); - - amount = parseInt(amount); - - let params = { - subtractFeeFromOutputs: amount === user.balance ? [0] : [] - }; - - let value = (amount / SATS).toFixed(8); - if (feeRate) params.feeRate = (feeRate / SATS).toFixed(8); - - let info = await lq.getAddressInfo(address); - - tx = await lq.createRawTransaction( - [], - [ - { - [address]: (amount / SATS).toFixed(8), - asset - } - ], - 0, - replaceable - ); - - l("funding tx for fee estimate", tx); - - tx = await lq.fundRawTransaction(tx, params); - - let blinded = await lq.blindRawTransaction(tx.hex); - let signed = await lq.signRawTransactionWithWallet(blinded); - - let decoded = await lq.decodeRawTransaction(signed.hex); - feeRate = Math.round((tx.fee * SATS * 1000) / decoded.vsize); - l("estimated", asset, feeRate); - - return { feeRate, tx }; -}; - -export default async (req, res) => { - try { - let tx = await liquidTx({ ...req.body, user: req.user }); - res.send(tx); - } catch (e) { - err("error estimating liquid fee", e.message, e.stack); - return res.code(500).send(e.message); - } -}; diff --git a/routes/liquid/generate.js b/routes/liquid/generate.js deleted file mode 100644 index 423d8b6a..00000000 --- a/routes/liquid/generate.js +++ /dev/null @@ -1,5 +0,0 @@ -export default async (req, res) => { - let address = await lq.getNewAddress(); - await lq.generateToAddress(1, address); - res.send({}); -}; diff --git a/routes/liquid/index.js b/routes/liquid/index.js deleted file mode 100644 index c4c50021..00000000 --- a/routes/liquid/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import broadcast from "./broadcast"; -import generate from "./generate"; -import fee from "./fee"; -import send from "./send"; - -export default { - broadcast, - generate, - fee, - send, -}; diff --git a/routes/liquid/receive.js b/routes/liquid/receive.js deleted file mode 100644 index 718feba5..00000000 --- a/routes/liquid/receive.js +++ /dev/null @@ -1,406 +0,0 @@ -import db from "$db"; -import config from "$config"; -import { getBlockHeight, getUser, getUserById, toSats, SATS } from "$lib/utils"; -import { emit } from "$lib/sockets"; -import store from "$lib/store"; -import { sendLiquid } from "$routes/liquid/send"; -import { notify } from "$lib/notifications"; -import { callWebhook } from "$lib/webhooks"; -import reverse from "buffer-reverse"; -import zmq from "zeromq/v5-compat"; -import { Op } from "@sequelize/core"; -import { fromBase58 } from "bip32"; -import bitcoin from "bitcoinjs-lib"; -import liquidJs from "liquidjs-lib"; -import { l, err, warn } from "$lib/logging"; -import lq from "$lib/liquid"; - -const { Block, networks, Transaction } = liquidJs; - -const network = - networks[ - config.liquid.network === "mainnet" ? "liquid" : config.liquid.network - ]; - -import { computeConversionFee } from "./conversionFee"; - -const getAccount = async (params, transaction) => { - let account = await db.Account.findOne({ - where: params, - lock: transaction.LOCK.UPDATE, - transaction - }); - - if (account) { - l("found account", params, account.asset, account.id); - return account; - } - - let { asset, pubkey } = params; - let name = asset.substr(0, 6); - let domain = ""; - let ticker = asset.substr(0, 3).toUpperCase(); - let precision = 8; - - if (pubkey) { - const nc = await db.Account.findOne({ - where: { pubkey }, - order: [["id", "ASC"]], - limit: 1, - lock: transaction.LOCK.UPDATE, - transaction - }); - - params.index = 0; - params.path = nc.path; - params.pubkey = nc.pubkey; - params.privkey = nc.privkey; - params.seed = nc.seed; - } - - if (store.assets[asset]) { - ({ domain, ticker, precision, name } = store.assets[asset]); - } else { - const existing = await db.Account.findOne({ - where: { - asset - }, - order: [["id", "ASC"]], - limit: 1, - lock: transaction.LOCK.UPDATE, - transaction - }); - - if (existing) { - l("existing", existing.id); - ({ domain, ticker, precision, name } = existing); - } - } - - params = { ...params, ...{ domain, ticker, precision, name } }; - params.balance = 0; - params.pending = 0; - params.network = "liquid"; - return db.Account.create(params, { transaction }); -}; - -const zmqRawBlock = zmq.socket("sub"); -zmqRawBlock.connect(config.liquid.zmqrawblock); -zmqRawBlock.subscribe("rawblock"); - -const zmqRawTx = zmq.socket("sub"); -zmqRawTx.connect(config.liquid.zmqrawtx); -zmqRawTx.subscribe("rawtx"); - -const queue = {}; -const seen = []; - -zmqRawTx.on("message", async (topic, message, sequence) => { - try { - const hex = message.toString("hex"); - - let unblinded, tx, blinded, txid; - try { - txid = Transaction.fromHex(hex).getId(); - if (seen.includes(txid)) return; - seen.push(txid); - if (seen.length > 5000) seen.shift(); - - unblinded = await lq.unblindRawTransaction(hex); - tx = await lq.decodeRawTransaction(unblinded.hex); - blinded = await lq.decodeRawTransaction(hex); - } catch (e) { - return err("problem decoding liquid tx", e.message); - } - - for (let o of tx.vout) { - try { - if (!(o.scriptPubKey && o.scriptPubKey.address)) return; - - const { asset } = o; - const value = toSats(o.value); - const { address } = o.scriptPubKey; - - if ( - Object.keys(store.addresses).includes(address) && - !store.change.includes(address) - ) { - await db.transaction(async transaction => { - let user = await getUser(store.addresses[address], transaction); - - let invoice = await db.Invoice.findOne({ - where: { - [Op.or]: { - unconfidential: address, - address - }, - user_id: user.id, - network: "liquid" - }, - order: [["id", "DESC"]], - transaction - }); - - if (!invoice) return; - - let confirmed = 0; - - let account = await db.Account.findOne({ - where: { - id: invoice.account_id, - asset - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - if (!account) { - let pubkey = account ? account.pubkey : null; - - account = await getAccount( - { - user_id: user.id, - asset, - pubkey - }, - transaction - ); - } - - await account.increment({ pending: value }, { transaction }); - await account.reload({ transaction }); - - if (config.liquid.walletpass) - await lq.walletPassphrase(config.liquid.walletpass, 300); - - await user.save({ transaction }); - - const currency = invoice ? invoice.currency : user.currency; - const rate = invoice ? invoice.rate : store.rates[user.currency]; - const tip = invoice ? invoice.tip : 0; - const memo = invoice ? invoice.memo : ""; - - let payment = await db.Payment.create( - { - account_id: account.id, - user_id: user.id, - hash: blinded.txid, - amount: value - tip, - currency, - memo, - rate, - received: true, - tip, - confirmed, - address, - network: "liquid", - invoice_id: invoice.id - }, - { transaction } - ); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - payment.invoice = invoice.get({ plain: true }); - - emit(user.username, "payment", payment); - emit(user.username, "account", payment.account); - l("liquid detected", address, user.username, asset, value); - notify(user, `${value} SAT payment detected`); - callWebhook(invoice, payment); - }); - } - } catch (e) { - console.log(e); - err("Problem processing transaction", e.message, e.stack); - } - } - } catch (e) { - console.log(e); - } -}); - -zmqRawBlock.on("message", async (topic, message, sequence) => { - try { - const payments = await db.Payment.findAll({ - where: { confirmed: 0 } - }); - - let hash, json; - - hash = await lq.getBlockHash(getBlockHeight(message)); - json = await lq.getBlock(hash, 2); - - for (let tx of json.tx) { - if (store.issuances[tx.txid]) { - await db.transaction(async transaction => { - const { - user_id, - asset, - asset_amount, - asset_payment_id, - token, - token_amount, - token_payment_id - } = store.issuances[tx.txid]; - - const user = await getUserById(user_id); - - let account = await db.Account.findOne({ - where: { user_id, asset }, - lock: transaction.LOCK.UPDATE, - transaction - }); - account.balance = Math.round(asset_amount * SATS); - account.pending = 0; - await account.save({ transaction }); - - let payment = await db.Payment.findOne({ - where: { id: asset_payment_id }, - include: { - model: db.Account, - as: "account" - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - payment.confirmed = true; - await payment.save({ transaction }); - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - - emit(user.username, "account", account); - emit(user.username, "payment", payment); - - if (token) { - account = await db.Account.findOne({ - where: { user_id, asset: token }, - lock: transaction.LOCK.UPDATE, - transaction - }); - account.balance = token_amount * SATS; - account.pending = 0; - await account.save({ transaction }); - - payment = await db.Payment.findOne({ - where: { id: token_payment_id }, - include: { - model: db.Account, - as: "account" - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - payment.confirmed = true; - await payment.save({ transaction }); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - - emit(user.username, "account", account); - emit(user.username, "payment", payment); - } - }); - } else if (payments.find(p => p.hash === tx.txid)) queue[tx.txid] = 1; - } - } catch (e) { - return console.log(e); - } -}); - -setInterval(async () => { - try { - const arr = Object.keys(queue); - - for (let i = 0; i < arr.length; i++) { - const hash = arr[i]; - - let account, invoice, address, user, total, p; - await db.transaction(async transaction => { - p = await db.Payment.findOne({ - where: { hash, confirmed: 0, received: 1 }, - include: [ - { - model: db.Account, - as: "account" - }, - { - model: db.Invoice, - as: "invoice" - }, - { - model: db.User, - as: "user" - } - ], - lock: transaction.LOCK.UPDATE, - transaction - }); - - if (p && p.address) address = p.address; - if (p && p.user) user = p.user; - - if (p && p.account) { - ({ account, invoice } = p); - total = p.amount + p.tip; - p.confirmed = 1; - await account.save({ transaction }); - - await invoice.increment({ received: total }, { transaction }); - await account.increment({ balance: total }, { transaction }); - await account.decrement( - { pending: Math.min(account.pending, total) }, - { transaction } - ); - await account.increment( - { liquid_credits: computeConversionFee(total) }, - { transaction } - ); - await account.reload({ transaction }); - await invoice.reload({ transaction }); - - await p.save({ transaction }); - - p = p.get({ plain: true }); - account = account.get({ plain: true }); - invoice = invoice.get({ plain: true }); - - emit(user.username, "account", account); - emit(user.username, "payment", p); - - l("liquid confirmed", user.username, account.asset, p.amount, p.tip); - - notify(user, `${total} SAT payment confirmed`); - callWebhook(p.invoice, p); - } else { - warn("couldn't find liquid payment", hash); - } - - delete queue[hash]; - }); - - let c = store.convert[address]; - if (address && c && p.account.asset === network.assetHash) { - l( - "liquid detected for conversion request", - address, - c.address, - user.username - ); - - user.account = p.account; - - sendLiquid({ - address: c.address, - amount: total - 100, - user, - limit: total - }); - } - } - } catch (e) { - err("problem processing queued liquid transaction", e.message, e.stack); - } -}, 1000); diff --git a/routes/liquid/send.js b/routes/liquid/send.js deleted file mode 100644 index 83b5d6ca..00000000 --- a/routes/liquid/send.js +++ /dev/null @@ -1,276 +0,0 @@ -import db from "$db"; -import lq from "$lib/liquid"; -import { toSats } from "$lib/utils"; -import { emit } from "$lib/sockets"; -import store from "$lib/store"; -import config from "$config"; -import { l, warn, err } from "$lib/logging"; -import { liquidTx } from "$routes/liquid/fee"; -import { Transaction } from 'liquidjs-lib'; -import { requirePin } from "$lib/utils"; - -import { - computeConversionFee, - conversionFeeReceiver -} from "./conversionFee.js"; - -const btc = config.liquid.btcasset; -const lcad = config.liquid.cadasset; - -export const sendLiquid = async ({ asset, amount, user, address, memo, tx, limit }) => { - try { - l("sending liquid", amount, address); - if (!tx) { - ({ tx } = await liquidTx({ - address, - asset, - amount, - feeRate: 200, - replaceable: false, - user - })); - } - - const isChange = async address => - (await lq.getAddressInfo(address)).ismine && - !Object.keys(store.addresses).includes(address); - - let totals = {}; - let change = {}; - let fee = 0; - - let { vout } = await lq.decodeRawTransaction(tx.hex); - - for (let i = 0; i < vout.length; i++) { - let { - asset, - value, - scriptPubKey - } = vout[i]; - - if (scriptPubKey.type === "fee") fee = toSats(value); - else { - if (!totals[asset]) totals[asset] = change[asset] = 0; - totals[asset] += toSats(value); - - if (scriptPubKey.address) { - if (await isChange(scriptPubKey.address)) { - change[asset] += toSats(value); - } - } - } - } - - const assets = Object.keys(totals); - const payments = []; - - let main, signed; - await db.transaction(async transaction => { - let fee_payment; - let fee_payment_id = null; - - for (let i = 0; i < assets.length; i++) { - let asset = assets[i]; - let amount = totals[asset]; - if (change[asset]) amount -= change[asset]; - let total = amount; - - if (asset === btc) { - let covered = 0; - let nonbtc = assets.filter(a => a !== btc); - if (nonbtc.length === 1) { - let faucet = await db.Account.findOne({ - where: { - asset: nonbtc[0], - user_id: null - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - if (faucet) { - covered = faucet.balance; - if (covered > fee) covered = fee; - await faucet.decrement({ balance: covered }, { transaction }); - await faucet.reload({ transaction }); - await faucet.save({ transaction }); - } - } - - total += fee - covered; - } - - var conversionFee = computeConversionFee(amount); - - if (limit && total > limit + fee) - throw new Error("Tx amount exceeds authorized amount"); - - if (asset !== btc || total) { - l("creating liquid payment", user.username, asset, total, fee); - - let account = await db.Account.findOne({ - where: { - user_id: user.id, - asset, - pubkey: null - }, - lock: transaction.LOCK.UPDATE, - order: [["balance", "DESC"]], - transaction - }); - - // use user's credits to reduce fee, if available - let conversionFeeDeduction = Math.min( - account.liquid_credits, - conversionFee - ); - if (conversionFeeDeduction) { - await account.decrement( - { liquid_credits: conversionFeeDeduction }, - { transaction } - ); - await account.reload({ transaction }); - conversionFee -= conversionFeeDeduction; - } - - if (asset !== btc) conversionFee = 0; - - if (total > account.balance) { - warn("amount exceeds balance", { - total, - fee, - balance: account.balance - }); - throw new Error( - `Insufficient funds, need ${total} ${ - account.ticker === "BTC" ? "SAT" : account.ticker - }, have ${account.balance}` - ); - } else if (total + conversionFee > account.balance) { - warn("amount plus conversion fee exceeds balance", { - total, - fee, - conversionFee, - balance: account.balance - }); - throw new Error( - `Insufficient funds, need ${total + conversionFee} ${ - account.ticker === "BTC" ? "SAT" : account.ticker - }, have ${account.balance}` - ); - } - - await account.decrement( - { balance: total + conversionFee }, - { transaction } - ); - await account.reload({ transaction }); - - let receiverAccount = await db.Account.findOne({ - where: { - "$user.username$": conversionFeeReceiver - }, - include: [ - { - model: db.User, - as: "user" - } - ], - lock: transaction.LOCK.UPDATE, - transaction - }); - - if (conversionFee) { - await receiverAccount.increment( - { balance: conversionFee }, - { transaction } - ); - await receiverAccount.reload({ transaction }); - fee_payment = { - amount: conversionFee, - fee: 0, - memo: "Liquid 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" - }; - fee_payment.account = receiverAccount; - ({ id: fee_payment_id } = await db.Payment.create(fee_payment, { - transaction - })); - } - - let payment = { - amount: -amount, - account_id: account.id, - fee, - memo, - user_id: user.id, - rate: store.rates[user.currency], - currency: user.currency, - address, - confirmed: true, - received: false, - network: "liquid", - fee_payment_id - }; - - payment.account = account; - payments.push(payment); - } - } - - signed = await lq.signRawTransactionWithWallet( - await lq.blindRawTransaction(tx.hex) - ); - let txid = Transaction.fromHex(signed.hex).getId(); - - for (let i = 0; i < payments.length; i++) { - let p = payments[i]; - if (p) { - let { account } = p; - p.hash = txid; - p = await db.Payment.create(p, { transaction }); - if (p.user_id === user.id && (account.ticker !== "BTC" || !main)) { - main = p.get({ plain: true }); - main.fee_payment = fee_payment; - main.account = account.get({ plain: true }); - } - } - } - - emit(user.username, "account", main.account); - emit(user.username, "payment", main); - }); - - if (config.liquid.walletpass) - await lq.walletPassphrase(config.liquid.walletpass, 300); - - let txid = await lq.sendRawTransaction(signed.hex); - l("sent liquid tx", txid, address); - - return main; - } catch (e) { - err("problem sending liquid", e.message); - return e; - } -}; - -export default async (req, res) => { - let { user } = req; - - try { - await requirePin(req); - let result = await sendLiquid({ ...req.body, user }) - if (result instanceof Error) throw result; - res.send(result); - } catch (e) { - console.log(e) - err("problem sending liquid", user.username, e.message); - return res.code(500).send(e); - } -}; diff --git a/routes/lnurl.js b/routes/lnurl.js index 1ad7b814..63488033 100644 --- a/routes/lnurl.js +++ b/routes/lnurl.js @@ -1,578 +1,66 @@ -import app from "$app"; -import config from "$config"; -import db from "$db"; -import store from "$lib/store"; -import { optionalAuth, auth } from "$lib/passport"; -import { l, err, warn } from "$lib/logging"; +import { g, s } from "$lib/db"; +import { l } from "$lib/logging"; +import { fail } from "$lib/utils"; +import { v4 } from "uuid"; +import got from "got"; +import { generate } from "$lib/invoices"; +import { bech32 } from "bech32"; -import ln from "$lib/ln"; -import persist from "$lib/persist"; -import { emit } from "$lib/sockets"; +export default { + async encode({ query: { address } }, res) { + let [name, domain] = address.split("@"); + let url = `https://${domain}/.well-known/lnurlp/${name}`; + let r = await got(url).json(); + if (r.tag !== "payRequest") fail("not an ln address"); + let enc = bech32.encode("lnurl", bech32.toWords(Buffer.from(url)), 20000); + res.send(enc); + }, -import axios from "axios"; -import lnurl from "lnurl"; -import jwt from "jsonwebtoken"; -import qs from "query-string"; -import bolt11 from "bolt11"; + async decode({ query: { text } }, res) { + let url = Buffer.from( + bech32.fromWords(bech32.decode(text, 20000).words) + ).toString(); -import { - computeConversionFee, - conversionFeeReceiver -} from "./lightning/conversionFee.js"; + res.send(await got(url).json()); + }, -import send from "$lib/send"; -import sendInternal from "$lib/sendInternal"; -import register from "$lib/register"; + async lnurlp({ params: { username } }, res) { + let uid = await g(`user:${username}`); + if (!uid) fail("user not found"); -const logins = persist("data/logins.json"); -const recipients = persist("data/recipients.json"); -const lnurlPayments = persist("data/payments.json"); -const withdrawals = persist("data/withdrawals.json"); + let id = v4(); + await s(`lnurl:${id}`, uid); + let { URL } = process.env; + let host = URL.split("/").at(-1); -const { HOSTNAME: hostname } = process.env; - -export const lnurlServer = lnurl.createServer(config.lnurl); - -app.get("/url", 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"); - } - } catch (e) { - err("couldn't find url", e.message); - } -}); - -app.get("/withdraw", auth, 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; - - res.send(result); - } catch (e) { - err("problem generating withdrawl url", e.message); - res.code(500).send(e.message); - } -}); - -app.post("/code", async (req, res, next) => { - try { - const { encoded } = req.body.lnurl; - const code = await db.Code.findOrCreate({ - where: { - code: `lnurl:${encoded.substr(-32)}`, - text: encoded - } - }); - - res.send(code); - } catch (e) { - err(e.errors); - res.code(500).send(e.message); - } -}); - -app.post("/withdraw", auth, async (req, res, next) => { - const { user } = req; - const { - amount: value, - params: { callback, k1 } - } = req.body; - - let invoice; - let label = new Date(); - let desc = label; - - invoice = await ln.invoice(`${value}sats`, label, desc); - - const { payment_request: pr } = invoice; - const url = `${callback}?k1=${k1}&pr=${pr}`; - - await db.Invoice.create({ - user_id: user.id, - text: pr, - rate: store.rates[user.currency], - currency: user.currency, - amount: value, - tip: 0, - network: "bitcoin" - }); - - try { - result = (await axios.get(url)).data; - res.send(result); - } catch (e) { - err("failed to withdraw", e.message); - res.code(500).send(e.message); - } -}); - -app.get("/lnurlp/:username", async (req, res, next) => { - try { - const { username } = req.params; - let user = await db.User.findOne({ - where: { - username - } - }); - - if (!user) throw new Error("user not found"); - - let { amount, minSendable, maxSendable } = req.query; - minSendable = minSendable || 1000; - maxSendable = maxSendable || 1000000000; - if (parseInt(amount)) minSendable = maxSendable = amount * 1000; - - let result = await lnurlServer.generateNewUrl("payRequest", { - minSendable, - maxSendable, - metadata: JSON.stringify([ - ["text/plain", `paying ${user.username}`], - ["text/identifier", `${user.username}@coinos.io`] - ]) - }); - - recipients[result.secret] = { - id: user.id, - currency: user.currency, - username - }; - - l("recipient", user.username, result.secret); - - result = await axios.get(result.url); - - res.send(result.data); - } catch (e) { - err("problem generating payment url", e.message); - res.code(500).send(e.message); - } -}); - -app.get("/pay/:username", async (req, res, next) => { - const { username } = req.params; - let user = await db.User.findOne({ - where: { - username - } - }); - - let { amount, minSendable, maxSendable } = req.query; - minSendable = minSendable || 1000; - maxSendable = maxSendable || 1000000000; - if (parseInt(amount)) minSendable = maxSendable = amount * 1000; - - try { - const result = await lnurlServer.generateNewUrl("payRequest", { - minSendable, - maxSendable, - metadata: JSON.stringify([ - ["text/plain", `paying ${user.username}`], - ["text/identifier", `${user.username}@coinos.io`] - ]) + res.send({ + metadata: [ + ["text/plain", `Paying ${username}@${host}`], + ["text/identifier", `${username}@${host}`] + ], + callback: `${URL}/lnurl/${id}`, + tag: "payRequest" }); + }, - recipients[result.secret] = { - id: user.id, - currency: user.currency, - username - }; - - l("recipient", user.username, result.secret); - res.send(result); - } catch (e) { - err("problem generating payment url", e.message); - res.code(500).send(e.message); - } -}); - -app.post("/pay", auth, async (req, res, next) => { - const { user } = req; - - const { - amount, - comment, - params: { callback } - } = req.body; - - let url = `${callback}${callback.includes("?") ? "&" : "?"}amount=${amount * - 1000}${comment ? "&comment=" + comment : ""}`; + async lnurl({ params: { id }, query: { amount } }, res) { + let pr = await g(`lnurlp:${id}`); - try { - const parts = callback.split("/"); - const secret = parts[parts.length - 1]; - lnurlPayments[secret] = user.id; + if (!pr) { + let uid = await g(`lnurl:${id}`); + let user = await g(`user:${uid}`); - if (recipients[secret]) { - let data = await sendInternal( - { + ({ text: pr } = await generate({ + invoice: { amount, - memo: comment, - username: recipients[secret].username + type: types.lightning }, user - ); + })); - res.send(data); - } else { - const { data } = await axios.get(url); - res.send(await send(amount, "", data.pr, user)); + await s(`lnurlp:${id}`, pr); } - } catch (e) { - console.log(e); - err("failed to send payment", e.message); - res.code(500).send(e.message); - } -}); - -app.get("/login", optionalAuth, async (req, res, next) => { - try { - const result = await lnurlServer.generateNewUrl("login"); - - if (req.user) { - logins[result.secret] = req.user.username; - } - - res.send(result); - } catch (e) { - err("problem generating login url", e.message); - res.code(500).send(e.message); - } -}); -app.get("/encode", async (req, res, next) => { - let { domain, name } = req.query; - let url = `https://${domain}/.well-known/lnurlp/${name}`; - res.send(lnurl.encode(url)); -}); - -app.get("/decode", 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) { - err("problem decoding lnurl", e.message); - res.code(500).send(e.message); - } -}); - -lnurlServer.on("payRequest:action:processed", async function(event) { - const { secret, params, result } = event; - const { id, invoice } = result; - const recipient = recipients[secret]; - let payreq = bolt11.decode(invoice); - - try { - let i = await db.Invoice.create({ - user_id: recipient.id, - text: invoice, - rate: store.rates[recipient.currency], - currency: recipient.currency, - amount: payreq.satoshis, - tip: 0, - network: "lightning" - }); - - l("invoice created", i.text, i.amount); - } catch (e) { - err("problem finding lnurl invoice", e.message); - } -}); - -lnurlServer.bindToHook( - "middleware:signedLnurl:afterCheckSignature", - async (req, res, next) => { - try { - let user; - const { amount: msats, key, tag, pr, k1 } = req.query; - - if (msats) { - let amount = Math.round(msats / 1000); - const parts = req.originalUrl.split("/"); - const secret = parts[parts.length - 1].split("?")[0]; - let user_id = lnurlPayments[secret]; - - if (user_id) { - let account = await db.Account.findOne({ - where: { - user_id, - asset: config.liquid.btcasset, - pubkey: null - } - }); - - if (account.balance < amount) { - throw new Error("Insufficient funds"); - } - } - } - - if (tag === "login") { - let username = logins[k1]; - - if (!username) { - const keyObj = await db.Key.findOne({ - where: { hex: key }, - include: [ - { - model: db.User, - as: "user" - } - ] - }); - - if (keyObj) ({ user } = keyObj); - - if (!user) { - let ip = - req.headers["x-forwarded-for"] || req.connection.remoteAddress; - - let username = `satoshi-${key.substr(0, 12)}`; - let existing = await db.User.findOne({ where: { username } }); - if (existing) user = existing; - else { - user = await register( - { - username, - password: key - }, - ip - ); - } - } - - ({ username } = user); - } - - logins[key] = { k1, username }; - } - - if (pr) { - user = withdrawals[k1]; - if (!user) { - if (next) next(new Error("withdrawal not found")); - return; - } - - try { - let amount; - - let { msatoshi } = await ln.decodepay(pr); - amount = parseInt(msatoshi / 1000); - let conversionFee = computeConversionFee(amount); - - await db.transaction(async transaction => { - let account = await db.Account.findOne({ - where: { - user_id: user.id, - asset: config.liquid.btcasset - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - - // account that receives conversion fees - let receiverAccount = await db.Account.findOne({ - where: { - "$user.username$": conversionFeeReceiver - }, - include: [ - { - model: db.User, - as: "user" - } - ], - lock: transaction.LOCK.UPDATE, - transaction - }); - - let conversionFeeDeduction = Math.min( - account.lightning_credits, - conversionFee - ); - if (conversionFeeDeduction) { - await account.decrement( - { lightning_credits: conversionFeeDeduction }, - { transaction } - ); - await account.reload({ transaction }); - conversionFee -= conversionFeeDeduction; - } - - if (account.balance < amount) { - throw new Error("Insufficient funds"); - } else if (account.balance < amount + conversionFee) { - throw new Error("Insufficient funds for conversion fee"); - } - - let fee_payment_id = null; - if (conversionFee) { - await receiverAccount.increment( - { balance: conversionFee }, - { transaction } - ); - await receiverAccount.reload({ transaction }); - let fee_payment = await db.Payment.create( - { - amount: conversionFee, - fee: 0, - memo: "Bitcoin 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; - } - - let payment = await db.Payment.create( - { - amount: -amount, - account_id: account.id, - user_id: user.id, - hash: pr, - rate: store.rates[user.currency], - currency: user.currency, - confirmed: true, - network: "lightning", - fee_payment_id - }, - { transaction } - ); - - setTimeout(async () => { - try { - let { payments } = await ln.getPayments({ - limit: 5 - }); - - let p = payments.find(p => p.request === pr); - if (p) { - l("found payment", pr); - payment.fee = p.fee; - await account.decrement({ balance: p.fee }, { transaction }); - await account.reload({ transaction }); - await payment.save(); - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - emit(user.username, "account", account); - emit(user.username, "payment", payment); - } else { - warn("payment not found, fee not set", pr); - } - } catch (e) { - err("problem trying to get ln withdrawal payment", e); - } - }, 1000); - - await account.decrement( - { balance: amount + conversionFee }, - { transaction } - ); - await account.reload({ transaction }); - - let p = payment.get({ plain: true }); - p.account = account.get({ plain: true }); - - emit(user.username, "account", p.account); - emit(user.username, "payment", p); - }); - } catch (e) { - err("failed to process withdrawal", e.message); - return next(e); - } - } - - if (next) next(); - } catch (e) { - err("unhandled lnurerr", e.message); - } - } -); - -lnurlServer.bindToHook("login", async key => { - try { - if (!key) throw new Error("login key not defined"); - - const exists = await db.Key.findOne({ - where: { hex: key }, - include: [{ model: db.User, as: "user" }] - }); - - let user; - if (logins[key] && logins[key] !== "undefined") { - const { username } = logins[key]; - user = await db.User.findOne({ - where: { username } - }); - - if (user) { - const [k, created] = await db.Key.findOrCreate({ - where: { - user_id: user.id, - hex: key - } - }); - - if (created) { - l("added key", username, k); - emit(username, "key", k); - } - } else { - l("user not found"); - user = await register({ - username: key.substr(0, 8), - password: key - }); - } - } else if (exists) ({ user } = exists); - - if (user && user.username) { - const payload = { username: user.username }; - const token = jwt.sign(payload, config.jwt); - const ws = store.sessions[logins[key].k1]; - if (ws && ws.send) - ws.send(JSON.stringify({ type: "token", data: { token, key } })); - } - } catch (e) { - err("problem with login hook", e.message); + res.send({ pr }); } -}); +}; diff --git a/routes/locations.js b/routes/locations.js index 3e0fee4c..fb4487f7 100644 --- a/routes/locations.js +++ b/routes/locations.js @@ -1,19 +1,7 @@ -import app from "$app"; -import redis from "$lib/redis"; -import axios from "axios"; +import { g } from "$lib/db"; -let locations; -app.get("/locations", async (req, res) => { - res.send({ locations }); -}); - -let getLocations = async () => { - let { data } = await axios.get('https://api.btcmap.org/v2/elements?updated_since=2022-09-19'); - locations = data.filter( - (l) => l['osm_json'].tags && l['osm_json'].tags['payment:coinos'] === 'yes' - ); - - setTimeout(getLocations, 60000); -} - -getLocations(); +export default { + async list(req, res) { + res.send({ locations: await g("locations") }); + } +}; diff --git a/routes/nostr.js b/routes/nostr.js new file mode 100644 index 00000000..005a4be9 --- /dev/null +++ b/routes/nostr.js @@ -0,0 +1,157 @@ +import { nada, uniq } from "$lib/utils"; +import config from "$config"; +import { g, s, db } from "$lib/db"; +import { pool, q } from "$lib/nostr"; + +export default { + async event({ params: { id } }, res) { + let event = await g(`ev:${id}`); + let { pubkey } = event; + + event.user = (await g(`user:${pubkey}`)) || { + username: pubkey.substr(0, 6), + pubkey, + anon: true, + follows: [], + followers: [] + }; + + res.send(event); + }, + + async notes({ params: { pubkey } }, res) { + let user = await g(`user:${pubkey}`); + + if (!user) + user = { + username: pubkey.substr(0, 6), + pubkey, + anon: true, + follows: [], + followers: [] + }; + + let params = { + kinds: [1], + authors: [pubkey] + }, + opts = { since: 0 }; + + q(`${pubkey}:notes`, params, opts).catch(nada); + + await s(`user:${pubkey}`, user); + + let ids = await db.sMembers(pubkey); + + let events = ids.length + ? (await db.mGet(ids.map(k => "ev:" + k))).map(JSON.parse) + : []; + + res.send(events.map(e => ({ ...e, user }))); + }, + + async broadcast(req, res) { + let { event } = req.body; + pool.send(["EVENT", event]); + res.send(event); + }, + + async follows({ params: { pubkey }, query: { tagsonly } }, res) { + let sub = `${pubkey}:follows`, + params = { + limit: 1, + kinds: [3], + authors: [pubkey] + }, + opts = { timeout: 60000, eager: 60000 }; + + q(sub, params, opts).catch(nada); + + let tags = (await g(`${pubkey}:follows`)) || []; + if (tagsonly) return res.send(tags); + + let follows = []; + for (let f of tags) { + let [_, pubkey] = f; + + q(`${pubkey}:profile:f1`, { + limit: 1, + kinds: [0], + authors: [pubkey] + }).catch(nada); + + let user = await g(`user:${pubkey}`); + + if (!user) + user = { + username: pubkey.substr(0, 6), + pubkey, + anon: true + }; + + follows.push(user); + } + + follows = uniq(follows, e => e.pubkey); + follows.sort((a, b) => a.username.localeCompare(b.username)); + + res.send(follows); + }, + + async followers({ params: { pubkey } }, res) { + try { + let pubkeys = [ + ...new Set([...(await db.sMembers(`${pubkey}:followers`))]) + ]; + + let followers = []; + + q( + `${pubkey}:followers`, + { kinds: [3], "#p": [pubkey] }, + { timeout: 60000, eager: 60000 } + ).catch(nada); + + for (let pubkey of pubkeys) { + let user = await g(`user:${pubkey}`); + if (!user || !user.updated) { + q(`${pubkey}:profile:f2`, { + limit: 1, + kinds: [0], + authors: [pubkey] + }).catch(nada); + + user = await g(`user:${pubkey}`); + } + + if (!user) + user = { + username: pubkey.substr(0, 6), + pubkey, + anon: true + }; + + followers.push(user); + } + + followers = uniq(followers, e => e.pubkey); + followers.sort((a, b) => a.username.localeCompare(b.username)); + + res.send(followers); + } catch (e) { + console.log(e); + res.code(500).send(e && e.message); + } + }, + + async identities(req, res) { + res.send({ + names: { + adam: + "566c166f3adab0c8fba5da015b0b3bcc8eb3696b455f2a1d43bfbd97059646a8", + asoltys: + "566c166f3adab0c8fba5da015b0b3bcc8eb3696b455f2a1d43bfbd97059646a8" + } + }); + } +}; diff --git a/routes/payments.js b/routes/payments.js index 416bc20a..1043959c 100644 --- a/routes/payments.js +++ b/routes/payments.js @@ -1,163 +1,258 @@ -import app from "$app"; -import db from "$db"; import config from "$config"; import store from "$lib/store"; - -import { auth, adminAuth, optionalAuth } from "$lib/passport"; -import fs from "fs"; -import { join } from "path"; -import { Op } from "@sequelize/core"; -import send from "./send"; -import { l, warn } from "$lib/logging"; -import { fail } from "$lib/utils"; +import { emit } from "$lib/sockets"; +import { v4 } from "uuid"; +import { db, g, s, t } from "$lib/db"; +import { l, err } from "$lib/logging"; +import { fail, btc, sats } from "$lib/utils"; +import { requirePin } from "$lib/auth"; +import { debit, credit, confirm, types } from "$lib/payments"; +import got from "got"; import bc from "$lib/bitcoin"; -import lq from "$lib/liquid"; +import ln from "$lib/ln"; -import btcRoutes from "./bitcoin/index"; -import lnRoutes from "./lightning/index"; -import lqRoutes from "./liquid/index"; +export default { + async create({ body, user }, res) { + let { amount, hash, maxfee, name, memo, payreq, tip, username } = body; -import "./bitcoin/receive"; -import "./lightning/receive"; -import "./liquid/receive"; + amount = parseInt(amount); + maxfee = parseInt(maxfee); + tip = parseInt(tip); -app.post("/send", auth, send); -app.post("/sendToTokenHolders", auth, async (req, res, next) => { - let { asset, amount } = req.body; + await requirePin({ body, user }); - let accounts = await db.Account.findAll({ - where: { - asset, - "$user.username$": { [Op.ne]: "gh" } - }, + let p; - include: [{ model: db.User, as: "user" }] - }); + if (username && username.endsWith("@classic")) { + let { username: source } = user; + username = username.replace("@classic", ""); + p = await debit(hash, amount, 0, memo, user, types.classic); - let totalTokens = accounts.reduce((a, b) => a + b.balance, 0); + await got + .post(`${config.classic}/admin/credit`, { + json: { username, amount, source }, + headers: { authorization: `Bearer ${config.admin}` } + }) + .json(); + } else if (payreq) { + p = await debit( + hash, + amount + maxfee, + maxfee, + memo, + user, + types.lightning + ); - let totalSats = Math.floor(amount / totalTokens); + let r = await ln.pay(payreq); - if (totalSats < 1) throw new Error("amount is too low to distribute"); + p.amount = -amount; + p.hash = r.payment_hash; + p.fee = r.msatoshi_sent - r.msatoshi; + p.ref = r.payment_preimage; - for (let i = 0; i < accounts.length; i++) { - let account = accounts[i]; - console.log(account.user.username, account.balance); - } - accounts.map(({ balance, user: { username } }) => ({ username, balance })); + await s(`payment:${p.id}`, p); + await db.incrBy(`balance:${p.uid}`, maxfee - p.fee); + } else if (hash) { + p = await debit(hash, amount, 0, memo, user); + await credit(hash, amount, memo, user.id); + } else { + let pot = name || v4(); + p = await debit(hash, amount, 0, memo, user, types.pot); + await db.incrBy(`pot:${pot}`, amount); + await db.lPush(`pot:${pot}:payments`, p.id); + l("funded pot", pot); + } - res.send({ success: "it worked" }); -}); + res.send(p); + }, -app.get("/except", adminAuth, (req, res) => { - let s = fs.createWriteStream("exceptions", { flags: "a" }); - store.unaccounted.map(tx => s.write(tx.txid + "\n")); - l("updated exceptions"); - res.send("updated exceptions"); -}); + async list({ user: { id }, query: { start, end, limit, offset } }, res) { + if (limit) limit = parseInt(limit); + offset = parseInt(offset) || 0; -app.post("/lightning/parse", lnRoutes.parse); -app.post("/lightning/channel", lnRoutes.channel); -app.post("/lightning/query", auth, lnRoutes.query); -app.post("/lightning/send", auth, lnRoutes.send); + let payments = (await db.lRange(`${id}:payments`, 0, -1)) || []; + payments = ( + await Promise.all( + payments.map(async id => { + let p = await g(`payment:${id}`); + if (p.created < start || p.created > end) return; + if (p.type === types.internal) p.with = await g(`user:${p.ref}`); + return p; + }) + ) + ) + .filter(p => p) + .sort((a, b) => b.created - a.created); -if (config.lnurl) { - let { channelRequest } = await import("$routes/lightning/channelRequest"); - app.post("/lightning/channelRequest", channelRequest); -} + let total = payments.length; -app.post("/bitcoin/broadcast", optionalAuth, btcRoutes.broadcast); -app.get("/bitcoin/generate", auth, btcRoutes.generate); -app.post("/bitcoin/sweep", auth, btcRoutes.sweep); -app.post("/bitcoin/fee", auth, btcRoutes.fee); -app.post("/bitcoin/send", auth, btcRoutes.send); + if (limit) payments = payments.slice(offset, offset + limit); -setTimeout(async () => { - try { - const address = await bc.getNewAddress(); - const { hdkeypath } = await bc.getAddressInfo(address); - const parts = hdkeypath.split("/"); - store.bcAddressIndex = parts[parts.length - 1].replace("'", ""); - } catch (e) { - console.error(e); - } -}, 50); + res.send({ payments, total }); + }, -app.post("/liquid/broadcast", optionalAuth, lqRoutes.broadcast); -app.get("/liquid/generate", auth, lqRoutes.generate); -app.post("/liquid/fee", auth, lqRoutes.fee); -app.post("/liquid/send", auth, lqRoutes.send); + async get({ params: { hash } }, res) { + res.send(await g(`payment:${hash}`)); + }, -setTimeout(async () => { - try { - const address = await lq.getNewAddress(); - const { hdkeypath } = await lq.getAddressInfo(address); - const parts = hdkeypath.split("/"); - store.lqAddressIndex = parts[parts.length - 1].slice(0, -1); - } catch (e) { - warn("Problem getting liquid address index", e.message); - } -}, 50); + async parse({ body: { payreq } }, res) { + let hour = 1000 * 60 * 60; + let { last } = store.nodes; + let { nodes } = store; -app.get("/payments", auth, async (req, res) => { - let { start, end, limit, offset, v2 } = req.query; + if (!last || last > Date.now() - hour) ({ nodes } = await ln.listnodes()); + store.nodes = nodes; - if (limit) limit = parseInt(limit); - if (offset) offset = parseInt(offset); + let twoWeeksAgo = new Date(new Date().setDate(new Date().getDate() - 14)); + let decoded = await ln.decodepay(payreq); + let { msatoshi, payee } = decoded; + let node = nodes.find(n => n.nodeid === payee); + let alias = node ? node.alias : payee.substr(0, 12); - let where = { - account_id: req.user.account_id, - }; + res.send({ alias, amount: Math.round(msatoshi / 1000) }); + }, - if (start || end) where.createdAt = {}; - if (start) where.createdAt[Op.gte] = new Date(parseInt(start)); - if (end) where.createdAt[Op.lte] = new Date(parseInt(end)); + async pot({ params: { name } }, res) { + let amount = await g(`pot:${name}`); + let payments = (await db.lRange(`pot:${name}:payments`, 0, -1)) || []; + payments = await Promise.all(payments.map(hash => g(`payment:${hash}`))); - if (!req.user.account_id) return res.send([]); + await Promise.all( + payments.map(async p => (p.user = await g(`user:${p.uid}`))) + ); - let total = await db.Payment.count({ where }); + payments = payments.filter(p => p); + res.send({ amount, payments }); + }, - let payments = await db.Payment.findAll({ - where, - order: [["id", "DESC"]], - include: { - model: db.Account, - as: "account" - }, - include: { - attributes: ["username", "profile"], - model: db.User, - as: "with" - }, - limit, - offset - }); + async take({ body: { name, amount }, user }, res) { + amount = parseInt(amount); + await t(`pot:${name}`, async (balance, db) => { + if (balance < amount) fail("Insufficient funds"); + await db + .multi() + .decrBy(`pot:${name}`, amount) + .exec(); + }); - if (v2) { - res.send({ transactions: payments, total }); - } else { - res.send(payments); - } -}); + let hash = v4(); + await s(`invoice:${hash}`, { + uid: user.id, + received: 0 + }); -app.get("/payment/:redeemcode", async (req, res) => { - try { - const { redeemcode } = req.params; - let payment = await db.Payment.findOne({ - where: { - redeemcode - }, - include: { - model: db.Account, - as: "account" + let payment = await credit(hash, amount, "", name, types.pot); + await db.lPush(`pot:${name}:payments`, hash); + + res.send({ payment }); + }, + + async bitcoin({ body: { txid, wallet } }, res) { + if (wallet === config.bitcoin.wallet) { + let { confirmations, details } = await bc.getTransaction(txid); + for (let { address, amount, vout } of details) { + if (confirmations > 0) { + await confirm(address, txid, vout); + } else { + await credit( + address, + sats(amount), + "", + `${txid}:${vout}`, + types.bitcoin + ); + } } + } + res.send({}); + }, + + async fee(req, res) { + let { amount, address, feeRate, subtract } = req.body; + let subtractFeeFromOutputs = subtract ? [0] : []; + let replaceable = true; + let outs = [{ [address]: btc(amount) }]; + let count = await bc.getBlockCount(); + + let raw = await bc.createRawTransaction([], outs, 0, replaceable); + let tx = await bc.fundRawTransaction(raw, { + feeRate, + subtractFeeFromOutputs, + replaceable }); - if (!payment) fail("invalid code"); + if (config.bitcoin.walletpass) + await bc.walletPassphrase(config.bitcoin.walletpass, 300); + + let { hex } = await bc.signRawTransactionWithWallet(tx.hex); + let { vsize } = await bc.decodeRawTransaction(hex); + feeRate = Math.round((sats(tx.fee) * 1000) / vsize); + + res.send({ feeRate, tx }); + }, + + async send(req, res) { + await requirePin(req); + + let { user } = req; + let { address, memo, tx } = req.body; + let { hex, fee } = tx; + + if (config.bitcoin.walletpass) + await bc.walletPassphrase(config.bitcoin.walletpass, 300); + + ({ hex } = await bc.signRawTransactionWithWallet(hex)); + + let r = await bc.testMempoolAccept([hex]); + if (!r[0].allowed) fail("transaction rejected"); + + fee = sats(fee); + if (fee < 0) fail("fee cannot be negative"); + + tx = await bc.decodeRawTransaction(hex); + let { txid } = tx; + + let total = 0; + let change = 0; + + for (let { + scriptPubKey: { address }, + value + } of tx.vout) { + total += sats(value); + if ( + (await bc.getAddressInfo(address)).ismine && + !(await g(`invoice:${address}`)) + ) + change += sats(value); + } + + total = total - change + fee; + let amount = total - fee; + + if (change && !amount) fail("Cannot send to unregistered coinos address"); + + await debit(txid, amount, fee, null, user, types.bitcoin, txid); + await bc.sendRawTransaction(hex); + + res.send({ txid }); + }, + + async del({ params: { username }, headers: { authorization } }, res) { + if (!authorization.includes(config.admin)) + return res.code(401).send("unauthorized"); + let { id, pubkey } = await g(`user:${await g(`user:${username}`)}`); + let invoices = await db.lRange(`${id}:invoices`, 0, -1); + let payments = await db.lRange(`${id}:payments`, 0, -1); + + for (let { id } of invoices) db.del(`invoice:${id}`); + for (let { id } of payments) db.del(`payment:${id}`); + db.del(`user:${username}`); + db.del(`user:${id}`); + db.del(`user:${pubkey}`); - res.send(payment); - } catch (e) { - res.code(500).send(e.message); + res.send({}); } -}); +}; diff --git a/routes/rates.js b/routes/rates.js new file mode 100644 index 00000000..c36c7ae8 --- /dev/null +++ b/routes/rates.js @@ -0,0 +1,11 @@ +import store from "$lib/store"; + +export default { + async last(req, res) { + res.send(store.last); + }, + + async index(req, res) { + res.send(store.rates); + } +}; diff --git a/routes/referrals.js b/routes/referrals.js deleted file mode 100644 index 7193964d..00000000 --- a/routes/referrals.js +++ /dev/null @@ -1,272 +0,0 @@ -import express from "express"; -var router = express.Router(); - -import { v4 } from "uuid"; - -router.get("/", function(req, res, next) { - res.send("list of referrals..."); -}); - -// Grant & Retrieve Referral tokens for existing user - -// usage: GET '/grant?sponsor_id=N&expiry=2021-09-01' -// -// Required: -// sponsor_id (= user.id) -// Changes to DB: -// adds record to referrals table: -// { token: <uuid>, sponsor_id: <user.id>, status: "available" } - -/** - * @api {post} /grant Grant Referral code - * @apiName Grant - * @apiGroup Referrals - * - * - * @apiPermission member - * - * @apiParam {sponsor_id} user_id of member generating referral code. - * @apiParam {expiry} optional expiry date for token. - * - * @apiSuccess {String} token referral token - * @apiSuccess {Date} expiry optional expiry - * - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "token": "*********", - * "status": "available", - * "expiry": null - * } - */ -router.post("/grant", auth, async (req, res) => { - const { sponsor_id, expiry } = req.body; - var token = v4(); - - var ref = await knex.table("referrals").insert({ - sponsor_id: sponsor_id, - token: token, - status: "available" - }); - - return res.send({ token: token, status: "available", expiry: expiry }); -}); - -/** - * @api {get} /grant Grant Referral code (variation using get) - * @apiName Grant - * @apiGroup Referrals - * - * @apiPermission member - * - * @apiPermission member - * @apiParam {sponsor_id} user_id of member generating referral code. - * @apiParam {expiry} optional expiry date for token. - * - * @apiSuccess {String} token referral token - * @apiSuccess {Status} status referral status - * @apiSuccess {Date} expiry optional expiry - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "token": "*********", - * "status": "available", - * "expiry": null - * } - */ -router.get("/grant", auth, async (req, res) => { - const { sponsor_id, expiry } = req.query; - var token = v4(); - - var ref = await knex.table("referrals").insert({ - sponsor_id: sponsor_id, - token: token, - status: "available" - }); - - return res.send({ token: token, status: "available", expiry: expiry }); -}); - -/** - * @api {get} /checkTokens/:sponsor_id Retrieve list of referral tokens generated by this user - * - * @apiName checkTokens - * @apiGroup Referrals - * - * - * @apiPermission member - * - * @apiParam {sponsor_id} user_id of member generating referral code. - * @apiParam {status} optional token status. - * - * @apiSuccess tokens array of token objects - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "tokens": [ - * {"token": "*********", "status": "available", "expiry": null} - * ] - * } - */ - -// TO FIX - change sponsor_id so that it is retrieved from current payload (NOT FROM URL) - -router.get("/checkTokens/:sponsor_id", auth, async (req, res) => { - const { sponsor_id } = req.params; - const { status } = req.query; - - var tokens = knex - .select( - "token", - "created_at", - knex.raw("LEFT(created_at,10) as created"), - "username", - "status" - ) - .from("referrals") - .leftJoin("users", "users.id", "referrals.user_id") - .where("sponsor_id", "like", sponsor_id); - - if (status && status !== "all") - tokens = tokens.where("status", "like", status); - const found = await tokens; - - return res.send({ tokens: found }); -}); - -/** - * @api {get} /verify/:user_id Verify token and apply to existing user - * - * @apiName verify - * @apiGroup Referrals - * - * @apiPermission member - * - * @apiParam {user_id} Current user - * @apiParam {token} Token to validate - * - * @apiSuccess verified = true - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "verified": true - * } - * - * @apiDescription referral token is updated from 'available' to 'used' - * @apiDescription referral token is updated with existing user_id - */ -router.get("/verify/:user_id/:token", auth, async (req, res) => { - const { user_id, token } = req.params; - - const found = await knex - .select("status") - .from("referrals") - .where("token", "like", token) - .whereNull("user_id"); - - if (found && found.length) { - if (found[0].status === "available") { - await knex("referrals") - .where("token", "like", token) - .update({ - status: "used", - user_id: user_id, - updated_at: new Date().toISOString().substring(0, 10) - }); - - return res.send({ - verified: true, - sponsor_id: found.sponsor_id, - updated: found.updated_at - }); - } else { - res.code(500).send({ - verified: false, - message: "Referral already " + found[0].status - }); - } - } else { - res.code(500).send("Invalid referral token"); - } -}); - -/** - * @api {post} /joinQueue Join waiting list (track email & phone) - * @apiName joinQueue - * @apiGroup Referrals - * - * @apiParam {email} email address - * @apiParam {phone} phone number - * - * @apiSuccess verified = true - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * "success": true - * "message": 'Added to waiting list' - * } - * - * @apiDescription referral token is updated from 'available' to 'used' - * @apiDescription referral token is updated with existing user_id - */ -router.post("/joinQueue", async (req, res) => { - const { email, phone, user_id } = req.body; - - await knex.table("waiting_list").insert({ - email: email, - phone: phone, - user_id: user_id - }); - - res.send({ - success: true, - message: "Added " + email + " to waiting list " + phone - }); -}); - -router.get("/joinQueue", async (req, res) => { - const { email, phone, user_id } = req.query; - - await knex("waiting_list").insert({ - email: email, - phone: phone, - user_id: user_id - }); - - res.send({ - success: true, - message: "Added " + email + " to waiting list " + phone - }); -}); - -/** - * @api {get} /isReferred/:user_id Check if user is referred - * @apiName isReferred - * @apiGroup Referrals - * - * @apiParam {user_id} user id - * - * @apiSuccessExample {json} Success-Response: - * HTTP/1.1 200 OK - * { - * true - * } - * - * @apiDescription returns boolean indicating if user is referred or not - */ -router.get("/isReferred/:user_id", async (req, res) => { - const { user_id } = req.params; - - var referred = await knex("referrals") - .select("referrals.id") - .where("user_id", "like", user_id) - .where("status", "like", "used"); - - if (referred && referred.length) { - res.send({ referred: true }); - } else { - res.send({ referred: false }); - } -}); - -export default router; diff --git a/routes/requests.js b/routes/requests.js index 70ab2b07..79af1fb6 100644 --- a/routes/requests.js +++ b/routes/requests.js @@ -1,137 +1,49 @@ -import app from "$app"; -import db from "$db"; -import { auth } from "$lib/passport"; import { emit } from "$lib/sockets"; -import { Op, col } from "@sequelize/core"; +import { g, db } from "$lib/db"; -app.get("/request/:id", auth, async (req, res) => { - try { - let request = await db.Request.findOne({ - where: { id: req.params.id }, - include: [ - { - attributes: ["username", "profile"], - model: db.User, - as: "recipient" - }, - { - attributes: ["username", "profile"], - model: db.User, - as: "requester" - }, - { - model: db.Invoice, - as: "invoice" - } - ] - }); +export default { + async get({ params: { id } }, res) { + try { + res.send({ request: await g(id) }); + } catch (e) { + console.log(e); + res.code(500).send(e.message); + } + }, - res.send({ request }); - } catch (e) { - console.log(e); - res.code(500).send(e.message); - } -}); - -app.get("/requests", auth, async (req, res) => { - try { - const day = new Date(new Date().setDate(new Date().getDate() - 1)); + async list({ user: { id } }, res) { + try { + const day = new Date(new Date().setDate(new Date().getDate() - 1)); - let invoices = ( - await db.Request.findAll({ - where: { - [Op.or]: { - recipient_id: req.user.id, - requester_id: req.user.id - }, - createdAt: { [Op.gt]: day } - }, - order: [["createdAt", "DESC"]], - include: [ - { - model: db.Invoice, - as: "invoice", - where: { received: { [Op.lt]: col("invoice.amount") } }, - include: { - model: db.User, - as: "user", - attributes: ["username", "profile"] - } - }, - { - model: db.User, - as: "requester", - attributes: ["username", "profile"] - } - ] - }) - ) - .map( - r => - r.invoice && { - request_id: r.id, - requester: r.requester.get({ plain: true }), - ...r.invoice.get({ plain: true }) - } - ) - .filter(r => r); + let invoices = await db.lRange(`${id}:invoices`, 0, -1); + let requests = await db.lRange(`${id}:requests`, 0, -1); - let requests = await db.Request.findAll({ - where: { - recipient_id: req.user.id, - createdAt: { [Op.gt]: day }, - invoice_id: { [Op.eq]: null } - }, - order: [["createdAt", "DESC"]], - include: { - attributes: ["username", "profile"], - model: db.User, - as: "requester" - } - }); - - res.send({ invoices, requests }); - } catch (e) { - console.log(e); - res.code(500).send(e.message); - } -}); + res.send({ invoices, requests }); + } catch (e) { + console.log(e); + res.code(500).send(e.message); + } + }, -app.post("/requests", auth, async (req, res) => { - try { - let { recipient: username, ...params } = req.body; - let { id: recipient_id } = await db.User.findOne({ where: { username } }); + async create( + { body: { recipient, ...params }, user: { username, profile } }, + res + ) { + let { id: recipient_id } = await g(`user:${recipient}`); - let request = await db.Request.create({ recipient_id, ...params }); - request = request.get({ plain: true }); + let request = { recipient_id, ...params }; request.requester = { - username: req.user.username, - profile: req.user.profile + username, + profile }; - emit(username, "request", request); - res.send(request); - } catch (e) { - console.log(e); - res.code(500).send(e.message); - } -}); + emit(recipient, "request", request); -app.post("/requests/delete", auth, async (req, res) => { - try { - let r = await db.Request.findOne({ - where: { - id: req.body.request_id, - [Op.or]: { requester_id: req.user.id, recipient_id: req.user.id } - } - }); - - if (!r) throw new Error("request not found"); - await r.destroy(); + res.send(request); + }, + async destroy({ body: { request_id }, user: { id } }, res) { + await db.lrem(`user:${id}:requests`, request_id); res.send(); - } catch (e) { - console.log(e); - res.code(500).send(e.message); } -}); +}; diff --git a/routes/send.js b/routes/send.js deleted file mode 100644 index 591b5ab7..00000000 --- a/routes/send.js +++ /dev/null @@ -1,14 +0,0 @@ -import { err } from "$lib/logging"; -import sendInternal from "$lib/sendInternal"; -import { requirePin } from "$lib/utils"; - -export default async (req, res, next) => { - try { - await requirePin(req); - let payment = await sendInternal(req.body, req.user); - res.send(payment); - } catch (e) { - err("problem sending internal payment", e.message); - return res.code(500).send(e.message); - } -}; diff --git a/routes/swaps.js b/routes/swaps.js deleted file mode 100644 index 169c2b8f..00000000 --- a/routes/swaps.js +++ /dev/null @@ -1,587 +0,0 @@ -import { emit } from "$lib/sockets"; -import app from "$app"; -import config from "$config"; -import { auth, optionalAuth } from "$lib/passport"; -import store from "$lib/store"; -import axios from "axios"; -import crypto from "crypto"; -import fs from "fs"; -import getAccount from "../lib/account"; -import { Op, col } from "@sequelize/core"; - -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 timeout = 20000; - -const swap = async (user, { a1, a2, v1, v2 }) => { - if (!parseInt(v1) || v1 < 0 || !parseInt(v2) || v2 < 0) - throw new Error("Invalid amount"); - 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("Asset not found on server"); - 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( - `placing order ${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: store.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]: v1 / v2 }, - 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; - let order = orders[i]; - if (order.v2 > v1) { - let rate = order.rate; - await order.decrement( - { - v2: v1 - }, - { transaction } - ); - await order.save({ transaction }); - await order.reload({ transaction }); - order.v1 = Math.round(order.v2 / rate); - await order.save({ transaction }); - - await order.acc1.increment({ balance: v2 }, { transaction }); - await order.acc1.reload({ transaction }); - - emit(order.user.username, "account", order.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: store.rates[user.currency], - confirmed: true, - received: true, - network: "COINOS" - }, - { transaction } - ); - - emit(user.username, "payment", payment.get({ plain: true })); - emit(user.username, "account", a1acc.get({ plain: true })); - - if (order.fee) { - const btc = await getAccount( - config.liquid.btcasset, - order.user, - transaction - ); - - payment = await db.Payment.create( - { - hash: "Swap Fee Refund", - amount: order.fee, - account_id: btc.id, - user_id: order.user_id, - currency: order.user.currency, - rate: store.rates[order.user.currency], - confirmed: true, - received: true, - network: "COINOS" - }, - { transaction } - ); - - await btc.increment({ balance: order.fee }, { transaction }); - await btc.reload({ transaction }); - - emit(order.user.username, "account", btc.get({ plain: true })); - } - - emit(order.user.username, "payment", payment.get({ plain: true })); - - order = order.get({ plain: true }); - order.a1 = order.acc1.asset; - order.a2 = order.acc2.asset; - order.rate = order.v2 / order.v1; - - broadcast("order", shallow(order)); - v1 = 0; - } else { - v1 -= order.v2; - v2 = Math.round(v1 * rate); - - order.accepted = true; - order.completedAt = new Date(); - - await order.save({ transaction }); - - let payment = await db.Payment.create( - { - hash: "Trade Fill", - amount: order.v1, - account_id: a2acc.id, - user_id: user.id, - currency: user.currency, - rate: store.rates[user.currency], - confirmed: true, - received: true, - network: "COINOS" - }, - { transaction } - ); - - await a2acc.increment({ balance: order.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: order.v2, - account_id: order.a2_id, - user_id: order.user_id, - currency: order.user.currency, - rate: store.rates[order.user.currency], - confirmed: true, - received: true, - network: "COINOS" - }, - { transaction } - ); - - emit(order.user.username, "payment", payment.get({ plain: true })); - await order.acc2.increment({ balance: order.v2 }, { transaction }); - await order.acc2.reload({ transaction }); - - order = order.get({ plain: true }); - order.a1 = order.acc1.asset; - order.a2 = order.acc2.asset; - order.rate = order.v2 / order.v1; - - broadcast("order", shallow(order)); - } - } - - let order; - if (v1) { - order = await db.Order.create( - { - v1, - v2, - user_id: user.id, - a1_id: a1acc.id, - a2_id: a2acc.id - }, - { 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: store.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: store.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, async (req, res) => { - const { id } = req.params; - const { user } = req; - - await cancel(user, id); - res.send({}); -}); - -app.post("/orders", auth, async (req, res) => { - return res.code(500).send("Trading temporarily disabled"); - const { user } = req; - try { - await swap(user, req.body); - res.send({}); - } catch (e) { - err(req.user.username, e.message); - res.code(500).send(e.message); - } -}); - -app.get("/orders", optionalAuth, 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(order => { - if (order.user_id !== req.user.id) order.user_id = null; - return order; - }) - ); - } catch (e) { - res.code(500).send(e.message); - } -}); - -if (config.maker) { - debug("setup maker account..."); - config.maker.map( - ({ amount, c1, c2, currency, askMultiplier, bidMultiplier }) => { - setInterval(async () => { - if (!rates) return; - - const user = await db.User.findOne({ - where: { - username: "maker" - } - }); - - if (!user) { - debug("maker user not defined in database"); - } else { - debug("maker: " + JSON.stringify(user)); - await db.transaction(async transaction => { - let order = await db.Order.findOne( - { - where: { - user_id: user.id, - "$acc1.asset$": c1, - "$acc2.asset$": c2, - accepted: false - }, - include: [ - { - model: db.Account, - as: "acc1" - }, - { - model: db.Account, - as: "acc2" - } - ] - }, - { transaction } - ); - - let params = { - a1: c1, - a2: c2, - v1: amount * SATS, - v2: Math.round( - amount * - SATS * - (((app.get("ask") * store.rates[currency]) / - store.rates["USD"]) * - askMultiplier) - ) - }; - - if (order) { - let bestBid = await db.Order.findOne( - { - where: { - "$acc1.asset$": c2, - "$acc2.asset$": c1, - accepted: false, - id: { - [Op.ne]: order.id - } - }, - include: [ - { - model: db.Account, - as: "acc1" - }, - { - model: db.Account, - as: "acc2" - } - ], - order: [["rate", "DESC"]], - limit: 1 - }, - { transaction } - ); - if ( - !bestBid || - (params.v2 / params.v1 > bestBid.rate && - order.acc2.balance >= params.v2) - ) { - order.v1 = params.v1; - order.v2 = params.v2; - await order.save(); - order = order.get({ plain: true }); - order.a1 = params.a1; - order.a2 = params.a2; - order.rate = order.v2 / order.v1; - - broadcast("order", shallow(order)); - } - } else { - try { - await swap(user, params); - } catch (e) { - warn("Failed to make ask", e.message); - } - } - - order = await db.Order.findOne( - { - where: { - user_id: user.id, - "$acc1.asset$": c2, - "$acc2.asset$": c1, - accepted: false - }, - include: [ - { - model: db.Account, - as: "acc1" - }, - { - model: db.Account, - as: "acc2" - } - ] - }, - { transaction } - ); - - params = { - a1: c2, - a2: c1, - v1: Math.round( - amount * - SATS * - ((app.get("bid") * store.rates[currency]) / - store.rates["USD"]) * - bidMultiplier - ), - v2: amount * SATS - }; - - if (order) { - let bestAsk = await db.Order.findOne( - { - where: { - "$acc1.asset$": c1, - "$acc2.asset$": c2, - accepted: false, - id: { - [Op.ne]: order.id - } - }, - include: [ - { - model: db.Account, - as: "acc1" - }, - { - model: db.Account, - as: "acc2" - } - ], - order: [["rate", "ASC"]], - limit: 1 - }, - transaction - ); - - if (bestAsk) - if ( - !bestAsk || - (params.v1 / params.v2 < bestAsk.rate && - order.acc1.balance >= params.v1) - ) { - order.v1 = params.v1; - order.v2 = params.v2; - await order.save(); - order = order.get({ plain: true }); - order.a1 = params.a1; - order.a2 = params.a2; - order.rate = order.v2 / order.v1; - - broadcast("order", shallow(order)); - } else { - broadcast("removeOrder", order.id); - await order.destroy({ transaction }); - } - } else { - try { - await swap(user, params); - } catch (e) { - warn("Failed to make bid", e.message); - } - } - }); - } - }, 5000); - } - ); -} diff --git a/routes/tickets.js b/routes/tickets.js deleted file mode 100644 index 7ca56740..00000000 --- a/routes/tickets.js +++ /dev/null @@ -1,22 +0,0 @@ -import app from "$app"; -import redis from "$lib/redis"; -import { auth } from "$lib/passport"; - -app.get("/ticket", async (req, res) => { - res.send({ ticket: await redis.lLen("tickets") }); -}); - -app.get("/tickets", async (req, res) => { - res.send(await redis.lRange("tickets", 0, -1)); -}); - -app.post("/ticket", auth, async (req, res) => { - try { - let { asset } = req.body; - if (!asset || asset.length !== 64) throw new Error("invalid asset"); - - res.send(await redis.rPush("tickets", req.body.asset)); - } catch (e) { - res.code(500).send(e.message); - } -}); diff --git a/routes/users.js b/routes/users.js index 02e7e3ed..17d4e1da 100644 --- a/routes/users.js +++ b/routes/users.js @@ -1,693 +1,394 @@ -import app from "$app"; +import { g, s, db } from "$lib/db"; import config from "$config"; import store from "$lib/store"; -import { auth, optionalAuth } from "$lib/passport"; -import { getUser } from "$lib/utils"; -import axios from "axios"; +import { fields, nada, pick, uniq, wait } from "$lib/utils"; import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import { authenticator } from "otplib"; -import getAccount from "$lib/account"; -import Sequelize from "@sequelize/core"; -import bitcoin from "bitcoinjs-lib"; -import liquid from "liquidjs-lib"; -import { fromBase58, fromPrivateKey } from "bip32"; -import { Mutex } from "async-mutex"; -import bip32 from "bip32"; import whitelist from "$lib/whitelist"; import { l, err, warn } from "$lib/logging"; -import db from "$db"; -import bc from "$lib/bitcoin"; -import lq from "$lib/liquid"; import { emit } from "$lib/sockets"; import register from "$lib/register"; import { requirePin } from "$lib/utils"; +import { v4 } from "uuid"; +import { parseISO } from "date-fns"; +import { types } from "$lib/payments"; -const pick = (O, ...K) => K.reduce((o, k) => ((o[k] = O[k]), o), {}); +import got from "got"; +import upload from "$lib/upload"; -app.get("/me", auth, async (req, res) => { - try { - let user = req.user.get({ plain: true }); - let payments = await req.user.getPayments({ - where: { - account_id: user.account_id - }, - order: [["id", "DESC"]], - limit: 12, - include: [ - { - model: db.Account, - as: "account" - }, - { - model: db.Payment, - as: "fee_payment" - } - ] - }); - user.accounts = await req.user.getAccounts(); - user.payments = payments; - user.haspin = !!user.pin; - res.send(pick(user, ...whitelist)); - } catch (e) { - res.code(500).send(e.message); - } -}); +export default { + upload, -app.get("/users/:username", async (req, res) => { - const { username } = req.params; + async me({ user }, res) { + try { + user.balance = await g(`balance:${user.id}`); + user.prompt = !!user.prompt; + res.send(pick(user, whitelist)); + } catch (e) { + console.log("problem fetching user", e); + res.code(500).send(e.message); + } + }, - const user = await db.User.findOne({ - attributes: ["username", "banner", "profile", "address", "currency"], - where: Sequelize.where( - Sequelize.fn("lower", Sequelize.col("username")), - username.toLowerCase() - ) - }); + async get({ params: { key } }, res) { + try { + if (key.startsWith("npub")) { + try { + key = Buffer.from(fromWords(decode(key).words)).toString("hex"); + } catch (e) {} + } - if (user) res.send(user); - else res.code(500).send("User not found"); -}); + let user = await g(`user:${key}`); + if (typeof user === "string") { + user = await g(`user:${user}`); + } -app.post("/register", async (req, res) => { - try { - const ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress; - const user = await register(req.body.user, ip, false); - res.send(pick(user, ...whitelist)); - } catch (e) { - res.code(500).send(e.message); - } -}); + if (!user && key.length === 64) { + user = { + currency: "USD", + username: key, + display: key.substr(0, 6), + pubkey: key, + anon: true + }; + } -app.post("/disable2fa", auth, async (req, res) => { - try { - let { - user, - body: { token } - } = req; + if (!user) return res.code(500).send("User not found"); - if (user.twofa && !authenticator.check(token, user.otpsecret)) { + let whitelist = [ + "username", + "banner", + "profile", + "address", + "currency", + "pubkey", + "display", + "prompt", + "id" + ]; + + user.prompt = !!user.prompt; + + res.send(pick(user, whitelist)); + } catch (e) { + console.log(e); + res.code(500).send(e.message); + } + }, + + async create(req, res) { + try { + const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress; + let { cipher, pubkey, password, username, salt } = req.body.user; + + let user = { + cipher, + pubkey, + password, + username, + salt + }; + + user = await register(user, ip, false); + l("registered new user", username); + res.send(pick(user, whitelist)); + } catch (e) { + res.code(500).send(e.message); + } + }, + + async disable2fa({ user, body: { token } }, res) { + let { id, twofa, username, otpsecret } = user; + if (twofa && !authenticator.check(token, otpsecret)) { return res.code(401).send("2fa required"); } user.twofa = false; - await user.save(); - emit(user.username, "user", user); - emit(user.username, "otpsecret", user.otpsecret); - l("disabled 2fa", user.username); + await s(`user:${id}`, user); + emit(username, "user", user); + emit(username, "otpsecret", user.otpsecret); + l("disabled 2fa", username); res.send({}); - } catch (e) { - res.code(500).send("Problem disabling 2fa"); - } -}); + }, -app.post("/2fa", auth, async (req, res) => { - let { user } = req; - try { - const isValid = authenticator.check(req.body.token, req.user.otpsecret); + async enable2fa({ user, body: { token } }, res) { + let { id, otpsecret, username } = user; + const isValid = authenticator.check(token, otpsecret); if (isValid) { user.twofa = true; - user.save(); - emit(user.username, "user", req.user); + await s(`user:${id}`, user); + emit(username, "user", user); } else { return res.code(500).send("Invalid token"); } - } catch (e) { - err("error setting up 2fa", e); - } - l("enabled 2fa", user.username); - res.send({}); -}); - -app.get("/exists", async (req, res) => { - let exists = await db.User.findOne({ - where: { username: req.query.username } - }); + l("enabled 2fa", username); + res.send({}); + }, - res.send(!!exists); -}); + async update({ user, body }, res) { + l("updating user", user.username); -app.post("/user", auth, async (req, res) => { - try { - let { user } = req; - let { - address, - confirm, - currencies, - currency, - email, - fiat, - password, - newpin, - pin, - seed, - tokens, - twofa, - unit, - username - } = req.body; + let { confirm, password, pin, newpin, username } = body; if (user.pin && !(pin === user.pin)) throw new Error("Pin required"); + if (typeof newpin !== "undefined") user.pin = newpin; + if (!user.pin || user.pin === "undefined") delete user.pin; - let exists = await db.User.findOne({ - where: { username } - }); + let exists; + if (username) exists = await g(`user:${username}`); let token; if (user.username !== username && exists) { err("username taken", username, user.username, exists.username); throw new Error("Username taken"); - } else { - store.sockets[username] = store.sockets[user.username]; + } else if (username) { if (user.username !== username) l("changing username", user.username, username); user.username = username; - - token = jwt.sign({ username }, config.jwt); - res.cookie("token", token, { - expires: new Date(Date.now() + 432000000) - }); } - if (unit) user.unit = unit; - user.currency = currency; - user.currencies = currencies; - user.pin = newpin; - user.tokens = tokens; - user.twofa = twofa; - user.seed = seed; - user.fiat = fiat; - user.email = email; - user.address = address; + let attributes = [ + "address", + "cipher", + "currencies", + "currency", + "display", + "email", + "fiat", + "locktime", + "prompt", + "pubkey", + "salt", + "seed", + "tokens", + "twofa" + ]; + + for (let a of attributes) { + if (body[a]) user[a] = body[a]; + } if (password && password === confirm) { user.password = await bcrypt.hash(password, 1); } - await user.save(); - - user = user.get({ plain: true }); user.haspin = !!user.pin; + await s(`user:${user.id}`, user); emit(user.username, "user", user); res.send({ user, token }); - } catch (e) { - err("error updating user", e.message); - res.code(500).send(e.message); - } -}); - -app.post("/keys", auth, async (req, res) => { - const { key: hex } = req.body; - const key = await db.Key.create({ - user_id: req.user.id, - hex - }); - emit(req.user.username, "key", key); - res.send(key); -}); - -app.post("/keys/delete", auth, async (req, res) => { - const { hex } = req.body; - ( - await db.Key.findOne({ - where: { - user_id: req.user.id, - hex - } - }) - ).destroy(); -}); - -app.post("/updateSeeds", auth, async (req, res) => { - let { user } = req; - let { seeds } = req.body; - let keys = Object.keys(seeds); - - for (let i = 0; i < keys.length; i++) { - let id = keys[i]; - let seed = seeds[id]; - await db.Account.update( - { seed }, - { - where: { id, user_id: user.id } - } - ); - - const account = await db.Account.findOne({ - where: { id } - }); - - emit(user.username, "account", account); - } - - res.send({}); -}); - -app.post("/accounts/delete", auth, async (req, res) => { - const { id } = req.body; - const account = await db.Account.findOne({ where: { id } }); - if (account) await account.destroy(); - res.send({}); -}); - -app.post("/accounts", auth, async (req, res) => { - const { - name, - seed, - pubkey, - ticker, - precision, - path, - privkey, - network - } = req.body; - const { user } = req; - - let account = await db.Account.create({ - user_id: user.id, - asset: config.liquid.btcasset, - balance: 0, - pending: 0, - name, - ticker, - precision, - pubkey, - privkey, - seed, - path, - network - }); - - emit(user.username, "account", account); - - if (pubkey) user.index++; - user.account_id = account.id; - await user.save(); - user.account = account; - emit(user.username, "user", user); - - res.send(account); -}); - -let login = async (req, res) => { - try { - const { params, sig, key } = req.body; - - if (sig) { - const { callback } = params; - - try { - const url = `${callback}&sig=${sig}&key=${key}`; - const response = await axios.get(url); - res.send(response.data); - } catch (e) { - err("problem calling lnurl login", e.message); - res.code(500).send(e.message); - } + }, - return; - } + async login(req, res) { + try { + const { username, password, token: twofa } = req.body; + l("logging in", username); - let twofa = req.body.token; + let uid = await g(`user:${username}`); + let user = await g(`user:${uid}`); - let user = await getUser(req.body.username); + if (!(user && user.migrated)) { + uid = user ? user.id : v4(); - if ( - !user || - (user.password && - !(await bcrypt.compare(req.body.password, user.password))) - ) { - warn("invalid username or password attempt", req.body.username); - return res.code(401).send({}); - } + let { classic } = config; + try { + let { token } = await got + .post(`${classic}/login`, { json: { username, password } }) + .json(); - if ( - user.twofa && - (typeof twofa === "undefined" || - !authenticator.check(twofa, user.otpsecret)) - ) { - return res.code(401).send("2fa required"); - } + if (!token) fail(); - l( - "login", - req.body.username, - req.headers["x-forwarded-for"] || req.connection.remoteAddress - ); + user = await got(`${classic}/admin/migrate/${username}?zero=true`, { + headers: { authorization: `Bearer ${config.admin}` } + }).json(); - let payload = { username: user.username }; - let token = jwt.sign(payload, config.jwt); - res.cookie("token", token, { expires: new Date(Date.now() + 432000000) }); - user.accounts = await user.getAccounts(); - user.keys = await user.getKeys(); - user = pick(user, ...whitelist); - res.send({ user, token }); - } catch (e) { - err("login error", e.message, req.connection.remoteAddress); - res.code(401).send({}); - } -}; + let { balance, pubkey } = user; + if (!user) fail(); -app.post("/login", login); -app.post("/taboggan", login); -app.post("/doggin", login); + uid = user.uuid; -app.post("/logout", optionalAuth, async (req, res) => { - let { subscription } = req.body; - if (!subscription) return res.send({}); + user = { + ...pick(user, fields), + id: uid, + about: user.address, + migrated: true + }; - const { username } = req.user; + await s(`user:${pubkey}`, uid); + await s(`user:${username}`, uid); + await s(`user:${uid}`, user); + await s(`balance:${uid}`, balance); - if (username) { - l("logging out", username); - let i = req.user.subscriptions.findIndex( - s => JSON.stringify(s) === subscription - ); - if (i > -1) { - req.user.subscriptions.splice(i, 1); - } - await req.user.save(); - Object.keys(logins).map( - k => logins[k]["username"] === username && delete logins[k] - ); - } + let payments = await got(`${classic}/payments`, { + headers: { authorization: `Bearer ${token}` } + }).json(); - res.send({}); -}); + for (let p of payments) { + let n = pick(p, ["amount", "fee", "confirmed", "rate", "currency"]); + n.id = v4(); + n.created = parseISO(p.createdAt).getTime(); + n.type = p.network; + if (!["bitcoin", "lightning"].includes(n.type)) + n.type = types.internal; -app.post("/account", auth, async (req, res) => { - const { user } = req; - const { - id, - name, - ticker, - precision, - domain, - seed, - pubkey, - privkey, - path, - hide, - index - } = req.body; + if (n.type === types.internal) { + if (!p.with) continue; - try { - await db.transaction(async transaction => { - let account = await db.Account.findOne({ - where: { id, user_id: user.id }, - lock: transaction.LOCK.UPDATE, - transaction - }); + let id = await g(`user:${p.with.username}`); + let u = id && (await g(`user:${id}`)); - let params = { - name, - ticker, - precision, - domain, - seed, - pubkey, - path, - hide, - index, - privkey - }; + if (!u) { + u = await got(`${classic}/admin/migrate/${p.with.username}`, { + headers: { authorization: `Bearer ${config.admin}` } + }).json(); - if (!account || (account.pubkey && !pubkey)) delete params.pubkey; + u = { id: u.uuid, about: u.address, ...pick(u, fields) }; + delete u.address; - await db.Account.update(params, { - where: { id, user_id: user.id }, - transaction - }); + await s(`user:${u.pubkey}`, u.id); + await s(`user:${p.with.username}`, u.id); + await s(`user:${u.id}`, u); - await account.reload({ transaction }); - emit(user.username, "account", account); - res.send({}); - }); - } catch (e) { - err("problem updating account", e.message); - return res.code(500).send("There was a problem updating the account"); - } -}); - -app.post("/shiftAccount", auth, async (req, res) => { - let { user } = req; - let { id } = req.body; + l("added missing user", u.username); + } - try { - const account = await db.Account.findOne({ - where: { id } - }); + if (!(u && u.id)) continue; + n.ref = u.id; + } - if (account.user_id !== user.id) - return res.code(500).send("Failed to open wallet"); + await s(`payment:${n.id}`, n); + await db.lPush(`${uid}:payments`, n.id); + } - user.account_id = account.id; - await user.save(); - let payments = await db.Payment.findAll({ - where: { - account_id: id - }, - order: [["id", "DESC"]], - limit: 12, - include: { - model: db.Account, - as: "account" + l("migrated user", user.username); + } catch (e) { + console.log(e); + } } - }); - - user = user.get({ plain: true }); - user.payments = payments; - user.account = account.get({ plain: true }); - - emit(user.username, "account", user.account); - emit(user.username, "user", user); - - res.send(user); - } catch (e) { - err("problem switching account", e.message); - return res.code(500).send("There was a problem switching accounts"); - } -}); - -app.get("/vapidPublicKey", function(req, res) { - res.send(config.vapid.publicKey); -}); - -app.post("/subscribe", auth, async function(req, res) { - let { subscriptions } = req.user; - let { subscription } = req.body; - if (!subscriptions) subscriptions = []; - if ( - !subscriptions.find(s => JSON.stringify(s) === JSON.stringify(subscription)) - ) - subscriptions.push(subscription); - req.user.subscriptions = subscriptions; - l("subscribing", req.user.username); - await req.user.save(); - res.sendStatus(201); -}); - -let redeeming = {}; -app.post("/redeem", optionalAuth, async function(req, res) { - const { redeemcode } = req.body; - try { - await db.transaction(async transaction => { - if (redeeming[redeemcode]) fail("redemption in progress"); - redeeming[redeemcode] = true; - if (!redeemcode) fail("no code provided"); - - let { user } = req; - - const source = await db.Payment.findOne({ - where: { - redeemcode: req.body.redeemcode - }, - include: { - model: db.Account, - as: "account" - }, - lock: transaction.LOCK.UPDATE, - transaction - }); - l("redeeming", redeemcode); - - if (!source) fail("Invalid code"); - if (source.redeemed) fail("Voucher has already been redeemed"); - let { amount } = source; - amount = -amount; - - if (!user) { - const ip = - req.headers["x-forwarded-for"] || req.connection.remoteAddress; - - user = await register( - { - username: redeemcode.substr(0, 8), - password: "" - }, - ip, - false - ); - - let payload = { username: user.username }; - let token = jwt.sign(payload, config.jwt); - res.cookie("token", token, { - expires: new Date(Date.now() + 432000000) - }); - - delete redeeming[redeemcode]; - return res.send({ user }); + if ( + !user || + (user.password && !(await bcrypt.compare(password, user.password))) + ) { + warn("invalid username or password attempt", username); + return res.code(401).send({}); } - let account = await getAccount(source.account.asset, user, transaction); - let { hash, memo, confirmed, fee, network } = source; - - source.redeemed = true; - await source.save({ transaction }); + if ( + user.twofa && + (typeof twofa === "undefined" || + !authenticator.check(twofa, user.otpsecret)) + ) { + return res.code(401).send("2fa required"); + } - let payment = await db.Payment.create( - { - amount, - account_id: account.id, - user_id: user.id, - hash: "Voucher " + redeemcode, - memo, - rate: store.rates[user.currency], - currency: user.currency, - confirmed, - network, - received: true, - fee - }, - { transaction } + l( + "login", + username, + req.headers["x-forwarded-for"] || req.socket.remoteAddress ); - await account.increment({ balance: amount }, { transaction }); - await account.reload({ transaction }); - - payment = payment.get({ plain: true }); - payment.account = account.get({ plain: true }); - emit(user.username, "payment", payment); - emit(user.username, "account", account); - - res.send({ payment }); - }); - } catch (e) { - delete redeeming[redeemcode]; - console.log(e); - err("problem redeeming", e.message); - return res.code(500).send("There was a problem redeeming the voucher"); - } -}); + let payload = { username, id: uid }; + let token = jwt.sign(payload, config.jwt); + res.cookie("token", token, { expires: new Date(Date.now() + 432000000) }); + user = pick(user, whitelist); + res.send({ user, token }); + } catch (e) { + console.log(e); + err("login error", e.message, req.socket.remoteAddress); + res.code(401).send({}); + } + }, -app.post("/checkRedeemCode", auth, async function(req, res) { - const { redeemcode } = req.body; + async logout(req, res) { + let { subscription } = req.body; + if (!subscription) return res.send({}); - const payment = await db.Payment.findOne({ where: { redeemcode } }); - res.send(payment); -}); + const { username } = req.user; -app.post("/password", auth, async function(req, res) { - const { user } = req; - const { password } = req.body; + if (username) { + l("logging out", username); + let i = req.user.subscriptions.findIndex( + s => JSON.stringify(s) === subscription + ); + if (i > -1) { + req.user.subscriptions.splice(i, 1); + } + await req.user.save(); + Object.keys(logins).map( + k => logins[k]["username"] === username && delete logins[k] + ); + } - if (!user.password) return res.send(true); - res.send(await bcrypt.compare(password, user.password)); -}); + res.send({}); + }, -app.get("/isInternal", auth, async function(req, res) { - let { user } = req; - let { address } = req.query; - if (!address) throw new Error("Address not provided"); + async subscribe({ body, user }, res) { + let { subscriptions } = user; + let { subscription } = body; + if (!subscriptions) subscriptions = []; + if ( + !subscriptions.find( + s => JSON.stringify(s) === JSON.stringify(subscription) + ) + ) + subscriptions.push(subscription); + user.subscriptions = subscriptions; + l("subscribing", user.username); + await user.save(); + res.sendStatus(201); + }, - let invoice = await db.Invoice.findOne({ - where: { address }, - include: { - attributes: ["username"], - model: db.User, - as: "user" - } - }); + async password({ body: { password }, user }, res) { + if (!user.password) return res.send(true); + res.send(await bcrypt.compare(password, user.password)); + }, - if (invoice) { - let info; + async otpsecret(req, res) { try { - info = await bc.getAddressInfo(address); + await requirePin(req); + let { otpsecret, username } = req.user; + res.send({ secret: otpsecret, username }); } catch (e) { - info = await lq.getAddressInfo(address); + res.code(500).send(e.message); } - if (info.ismine) { - emit(user.username, "to", invoice.user); - return res.send(true); - } - } + }, - res.send(false); -}); + async contacts({ user: { id } }, res) { + let i = (await g(`${id}:cindex`)) || 0; + let payments = (await db.lRange(`${id}:payments`, i, -1)) || []; + await db.incrBy(`${id}:cindex`, payments.length); -app.get("/invoices", auth, async function(req, res) { - let invoices = await db.Invoice.findAll({ - where: { user_id: req.user.id } - }); - res.send(invoices); -}); + let contacts = (await g(`${id}:contacts`)) || []; -app.post("/signMessage", auth, async function(req, res) { - let { address, message } = req.body; + for (let { ref } of ( + await Promise.all(payments.map(async id => await g(`payment:${id}`))) + ).filter(p => p.type === types.internal && p.ref)) { + !~contacts.findIndex(({ id }) => id === ref) && + contacts.push(await g(`user:${ref}`)); + } - let invoices = await db.Invoice.findAll({ - where: { user_id: req.user.id } - }); + await s(`${id}:contacts`, contacts); - if (invoices.find(i => i.address === address)) { - if (config.bitcoin.walletpass) - await bc.walletPassphrase(config.bitcoin.walletpass, 300); - return res.send(await bc.signMessage(address, message)); - } + res.send(contacts); + }, - res.code(500).send("Address not found for user"); -}); + async del({ params: { username }, headers: { authorization } }, res) { + if (!(authorization && authorization.includes(config.admin))) + return res.code(401).send("unauthorized"); -app.post("/otpsecret", auth, async function(req, res) { - try { - await requirePin(req); - let { otpsecret } = req.user; - res.send({ secret: otpsecret }); - } catch (e) { - res.code(500).send(e.message); - } -}); + let { id, pubkey } = await g(`user:${await g(`user:${username}`)}`); + let invoices = await db.lRange(`${id}:invoices`, 0, -1); + let payments = await db.lRange(`${id}:payments`, 0, -1); -app.get("/contacts", auth, async function(req, res) { - try { - let contacts = await db.Payment.findAll({ - where: { - user_id: req.user.id, - with_id: { [Sequelize.Op.ne]: null } - }, - include: { - attributes: ["username", "profile"], - model: db.User, - as: "with" - }, - attributes: [ - [Sequelize.fn("max", Sequelize.col("payments_model.createdAt")), "last"] - ], - group: ["with_id"], - order: [[{ model: db.User, as: "with" }, "username", "ASC"]] - }); + for (let { id } of invoices) db.del(`invoice:${id}`); + for (let { id } of payments) db.del(`payment:${id}`); + db.del(`user:${username}`); + db.del(`user:${id}`); + db.del(`user:${pubkey}`); - res.send( - contacts.map(c => { - c = c.get({ plain: true }); - return { ...c.with, last: c.last }; - }) - ); - } catch (e) { - console.log(e); - res.code(500).send(e.message); + res.send({}); } -}); +}; |
