2020-07-11 15:20:05 +01:00

514 lines
16 KiB
JavaScript

import { User, Lock, Paym, Invo } from '../class/';
import Frisbee from 'frisbee';
const config = require('../config');
let express = require('express');
let router = express.Router();
let logger = require('../utils/logger');
console.log('using config', JSON.stringify(config));
var Redis = require('ioredis');
var redis = new Redis(config.redis);
redis.monitor(function(err, monitor) {
monitor.on('monitor', function(time, args, source, database) {
// console.log('REDIS', JSON.stringify(args));
});
});
let bitcoinclient = require('../bitcoin');
let lightning = require('../lightning');
let identity_pubkey = false;
// ###################### SMOKE TESTS ########################
bitcoinclient.request('getblockchaininfo', false, function(err, info) {
if (info && info.result && info.result.blocks) {
if (info.result.chain === 'mainnet' && info.result.blocks < 550000) {
console.error('bitcoind is not caught up');
process.exit(1);
}
} else {
console.error('bitcoind failure:', err, info);
process.exit(2);
}
});
lightning.getInfo({}, function(err, info) {
if (err) {
console.error('lnd failure');
console.dir(err);
process.exit(3);
}
if (info) {
console.info(info);
if (!info.synced_to_chain) {
console.error('lnd not synced');
process.exit(4);
}
identity_pubkey = info.identity_pubkey;
}
});
redis.info(function (err, info) {
if (err || !info) {
console.error('redis failure');
process.exit(5);
}
});
let subscribeInvoicesCall = lightning.subscribeInvoices({});
subscribeInvoicesCall.on('data', async function (response) {
if (response.state === 'SETTLED') {
const LightningInvoiceSettledNotification = {
memo: response.memo,
preimage: response.r_preimage.toString('hex'),
hash: response.r_hash.toString('hex'),
amt_paid_sat: response.amt_paid_sat,
};
// obtaining a lock, to make sure we push to groundcontrol only once
// since this web server can have several instances running, and each will get the same callback from LND
// and dont release the lock - it will autoexpire in a while
let lock = new Lock(redis, 'groundcontrol_hash_' + LightningInvoiceSettledNotification.hash);
if (!(await lock.obtainLock())) {
return;
}
let invoice = new Invo(redis, bitcoinclient, lightning);
await invoice._setIsPaymentHashPaidInDatabase(LightningInvoiceSettledNotification.hash, true);
console.log('payment', LightningInvoiceSettledNotification.hash, 'was paid, posting to GroundControl...');
const baseURI = process.env.GROUNDCONTROL;
if (!baseURI) return;
const _api = new Frisbee({ baseURI: baseURI });
const apiResponse = await _api.post(
'/lightningInvoiceGotSettled',
Object.assign(
{},
{
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
},
body: LightningInvoiceSettledNotification,
},
),
);
console.log('GroundControl:', apiResponse.originalResponse.status);
}
});
subscribeInvoicesCall.on('status', function (status) {
// The current status of the stream.
});
subscribeInvoicesCall.on('end', function () {
// The server has closed the stream.
});
// ######################## ROUTES ########################
const rateLimit = require('express-rate-limit');
const postLimiter = rateLimit({
windowMs: 30 * 60 * 1000,
max: 100,
});
router.post('/create', postLimiter, async function(req, res) {
logger.log('/create', [req.id]);
if (!(req.body.partnerid && req.body.partnerid === 'bluewallet' && req.body.accounttype)) return errorBadArguments(res);
let u = new User(redis, bitcoinclient, lightning);
await u.create();
await u.saveMetadata({ partnerid: req.body.partnerid, accounttype: req.body.accounttype, created_at: new Date().toISOString() });
res.send({ login: u.getLogin(), password: u.getPassword() });
});
router.post('/auth', postLimiter, async function(req, res) {
logger.log('/auth', [req.id]);
if (!((req.body.login && req.body.password) || req.body.refresh_token)) return errorBadArguments(res);
let u = new User(redis, bitcoinclient, lightning);
if (req.body.refresh_token) {
// need to refresh token
if (await u.loadByRefreshToken(req.body.refresh_token)) {
res.send({ refresh_token: u.getRefreshToken(), access_token: u.getAccessToken() });
} else {
return errorBadAuth(res);
}
} else {
// need to authorize user
let result = await u.loadByLoginAndPassword(req.body.login, req.body.password);
if (result) res.send({ refresh_token: u.getRefreshToken(), access_token: u.getAccessToken() });
else errorBadAuth(res);
}
});
router.post('/addinvoice', postLimiter, async function(req, res) {
logger.log('/addinvoice', [req.id]);
let u = new User(redis, bitcoinclient, lightning);
if (!(await u.loadByAuthorization(req.headers.authorization))) {
return errorBadAuth(res);
}
logger.log('/addinvoice', [req.id, 'userid: ' + u.getUserId()]);
if (!req.body.amt || /*stupid NaN*/ !(req.body.amt > 0)) return errorBadArguments(res);
lightning.addInvoice({ memo: req.body.memo, value: req.body.amt, expiry: 3600 * 24 }, async function(err, info) {
if (err) return errorLnd(res);
info.pay_req = info.payment_request; // client backwards compatibility
await u.saveUserInvoice(info);
res.send(info);
});
});
router.post('/payinvoice', async function(req, res) {
let u = new User(redis, bitcoinclient, lightning);
if (!(await u.loadByAuthorization(req.headers.authorization))) {
return errorBadAuth(res);
}
logger.log('/payinvoice', [req.id, 'userid: ' + u.getUserId(), 'invoice: ' + req.body.invoice]);
if (!req.body.invoice) return errorBadArguments(res);
let freeAmount = false;
if (req.body.amount) {
freeAmount = parseInt(req.body.amount);
if (freeAmount <= 0) return errorBadArguments(res);
}
// obtaining a lock
let lock = new Lock(redis, 'invoice_paying_for_' + u.getUserId());
if (!(await lock.obtainLock())) {
return errorGeneralServerError(res);
}
let userBalance;
try {
userBalance = await u.getCalculatedBalance();
} catch (Error) {
logger.log('', [req.id, 'error running getCalculatedBalance():', Error.message]);
lock.releaseLock();
return errorTryAgainLater(res);
}
lightning.decodePayReq({ pay_req: req.body.invoice }, async function(err, info) {
if (err) {
await lock.releaseLock();
return errorNotAValidInvoice(res);
}
if (+info.num_satoshis === 0) {
// 'tip' invoices
info.num_satoshis = freeAmount;
}
logger.log('/payinvoice', [req.id, 'userBalance: ' + userBalance, 'num_satoshis: ' + info.num_satoshis]);
if (userBalance >= +info.num_satoshis + Math.floor(info.num_satoshis * 0.01)) {
// got enough balance, including 1% of payment amount - reserve for fees
if (identity_pubkey === info.destination) {
// this is internal invoice
// now, receiver add balance
let userid_payee = await u.getUseridByPaymentHash(info.payment_hash);
if (!userid_payee) {
await lock.releaseLock();
return errorGeneralServerError(res);
}
if (await u.getPaymentHashPaid(info.payment_hash)) {
// this internal invoice was paid, no sense paying it again
await lock.releaseLock();
return errorLnd(res);
}
let UserPayee = new User(redis, bitcoinclient, lightning);
UserPayee._userid = userid_payee; // hacky, fixme
await UserPayee.clearBalanceCache();
// sender spent his balance:
await u.clearBalanceCache();
await u.savePaidLndInvoice({
timestamp: parseInt(+new Date() / 1000),
type: 'paid_invoice',
value: +info.num_satoshis + Math.floor(info.num_satoshis * Paym.fee),
fee: Math.floor(info.num_satoshis * Paym.fee),
memo: decodeURIComponent(info.description),
pay_req: req.body.invoice,
});
await UserPayee.setPaymentHashPaid(info.payment_hash);
await lock.releaseLock();
return res.send(info);
}
// else - regular lightning network payment:
var call = lightning.sendPayment();
call.on('data', async function(payment) {
// payment callback
await u.unlockFunds(req.body.invoice);
if (payment && payment.payment_route && payment.payment_route.total_amt_msat) {
let PaymentShallow = new Paym(false, false, false);
payment = PaymentShallow.processSendPaymentResponse(payment);
payment.pay_req = req.body.invoice;
payment.decoded = info;
await u.savePaidLndInvoice(payment);
await u.clearBalanceCache();
lock.releaseLock();
res.send(payment);
} else {
// payment failed
lock.releaseLock();
return errorPaymentFailed(res);
}
});
if (!info.num_satoshis) {
// tip invoice, but someone forgot to specify amount
await lock.releaseLock();
return errorBadArguments(res);
}
let inv = {
payment_request: req.body.invoice,
amt: info.num_satoshis, // amt is used only for 'tip' invoices
fee_limit: { fixed: Math.floor(info.num_satoshis * 0.005) + 1 },
};
try {
await u.lockFunds(req.body.invoice, info);
call.write(inv);
} catch (Err) {
await lock.releaseLock();
return errorPaymentFailed(res);
}
} else {
await lock.releaseLock();
return errorNotEnougBalance(res);
}
});
});
router.get('/getbtc', async function(req, res) {
logger.log('/getbtc', [req.id]);
let u = new User(redis, bitcoinclient, lightning);
await u.loadByAuthorization(req.headers.authorization);
if (!u.getUserId()) {
return errorBadAuth(res);
}
let address = await u.getAddress();
if (!address) {
await u.generateAddress();
address = await u.getAddress();
}
res.send([{ address }]);
});
router.get('/checkpayment/:payment_hash', async function(req, res) {
logger.log('/checkpayment', [req.id]);
let u = new User(redis, bitcoinclient, lightning);
await u.loadByAuthorization(req.headers.authorization);
if (!u.getUserId()) {
return errorBadAuth(res);
}
let paid = true;
if (!(await u.getPaymentHashPaid(req.params.payment_hash))) { // Not found on cache
paid = await u.syncInvoicePaid(req.params.payment_hash);
}
res.send({paid: paid});
});
router.get('/balance', postLimiter, async function(req, res) {
let u = new User(redis, bitcoinclient, lightning);
try {
logger.log('/balance', [req.id]);
if (!(await u.loadByAuthorization(req.headers.authorization))) {
return errorBadAuth(res);
}
logger.log('/balance', [req.id, 'userid: ' + u.getUserId()]);
if (!(await u.getAddress())) await u.generateAddress(); // onchain address needed further
await u.accountForPosibleTxids();
let balance = await u.getBalance();
if (balance < 0) balance = 0;
res.send({ BTC: { AvailableBalance: balance } });
} catch (Error) {
logger.log('', [req.id, 'error getting balance:', Error.message, 'userid:', u.getUserId()]);
return errorGeneralServerError(res);
}
});
router.get('/getinfo', postLimiter, async function(req, res) {
logger.log('/getinfo', [req.id]);
let u = new User(redis, bitcoinclient, lightning);
if (!(await u.loadByAuthorization(req.headers.authorization))) {
return errorBadAuth(res);
}
lightning.getInfo({}, function(err, info) {
if (err) return errorLnd(res);
res.send(info);
});
});
router.get('/gettxs', async function(req, res) {
logger.log('/gettxs', [req.id]);
let u = new User(redis, bitcoinclient, lightning);
if (!(await u.loadByAuthorization(req.headers.authorization))) {
return errorBadAuth(res);
}
logger.log('/gettxs', [req.id, 'userid: ' + u.getUserId()]);
if (!(await u.getAddress())) await u.generateAddress(); // onchain addr needed further
try {
await u.accountForPosibleTxids();
let txs = await u.getTxs();
let lockedPayments = await u.getLockedPayments();
for (let locked of lockedPayments) {
txs.push({
type: 'paid_invoice',
fee: Math.floor(locked.amount * 0.01) /* feelimit */,
value: locked.amount + Math.floor(locked.amount * 0.01) /* feelimit */,
timestamp: locked.timestamp,
memo: 'Payment in transition',
});
}
res.send(txs);
} catch (Err) {
logger.log('', [req.id, 'error gettxs:', Err.message, 'userid:', u.getUserId()]);
res.send([]);
}
});
router.get('/getuserinvoices', postLimiter, async function(req, res) {
logger.log('/getuserinvoices', [req.id]);
let u = new User(redis, bitcoinclient, lightning);
if (!(await u.loadByAuthorization(req.headers.authorization))) {
return errorBadAuth(res);
}
logger.log('/getuserinvoices', [req.id, 'userid: ' + u.getUserId()]);
try {
let invoices = await u.getUserInvoices(req.query.limit);
res.send(invoices);
} catch (Err) {
logger.log('', [req.id, 'error getting user invoices:', Err.message, 'userid:', u.getUserId()]);
res.send([]);
}
});
router.get('/getpending', async function(req, res) {
logger.log('/getpending', [req.id]);
let u = new User(redis, bitcoinclient, lightning);
if (!(await u.loadByAuthorization(req.headers.authorization))) {
return errorBadAuth(res);
}
logger.log('/getpending', [req.id, 'userid: ' + u.getUserId()]);
if (!(await u.getAddress())) await u.generateAddress(); // onchain address needed further
await u.accountForPosibleTxids();
let txs = await u.getPendingTxs();
res.send(txs);
});
router.get('/decodeinvoice', async function(req, res) {
logger.log('/decodeinvoice', [req.id]);
let u = new User(redis, bitcoinclient, lightning);
if (!(await u.loadByAuthorization(req.headers.authorization))) {
return errorBadAuth(res);
}
if (!req.query.invoice) return errorGeneralServerError(res);
lightning.decodePayReq({ pay_req: req.query.invoice }, function(err, info) {
if (err) return errorNotAValidInvoice(res);
res.send(info);
});
});
router.get('/checkrouteinvoice', async function(req, res) {
logger.log('/checkrouteinvoice', [req.id]);
let u = new User(redis, bitcoinclient, lightning);
if (!(await u.loadByAuthorization(req.headers.authorization))) {
return errorBadAuth(res);
}
if (!req.query.invoice) return errorGeneralServerError(res);
// at the momment does nothing.
// TODO: decode and query actual route to destination
lightning.decodePayReq({ pay_req: req.query.invoice }, function(err, info) {
if (err) return errorNotAValidInvoice(res);
res.send(info);
});
});
module.exports = router;
// ################# HELPERS ###########################
function errorBadAuth(res) {
return res.send({
error: true,
code: 1,
message: 'bad auth',
});
}
function errorNotEnougBalance(res) {
return res.send({
error: true,
code: 2,
message: 'not enough balance. Make sure you have at least 1% reserved for potential fees',
});
}
function errorNotAValidInvoice(res) {
return res.send({
error: true,
code: 4,
message: 'not a valid invoice',
});
}
function errorLnd(res) {
return res.send({
error: true,
code: 7,
message: 'LND failue',
});
}
function errorGeneralServerError(res) {
return res.send({
error: true,
code: 6,
message: 'Something went wrong. Please try again later',
});
}
function errorBadArguments(res) {
return res.send({
error: true,
code: 8,
message: 'Bad arguments',
});
}
function errorTryAgainLater(res) {
return res.send({
error: true,
code: 9,
message: 'Your previous payment is in transit. Try again in 5 minutes',
});
}
function errorPaymentFailed(res) {
return res.send({
error: true,
code: 10,
message: 'Payment failed. Does the receiver have enough inbound capacity?',
});
}