Save exchange rate with invoice

- Fix issue where completed invoices were flagged as expired
This commit is contained in:
2021-01-02 22:14:59 +01:00
parent b356f3ee70
commit fc71aed660
8 changed files with 55 additions and 44 deletions

View File

@@ -72,7 +72,6 @@ export async function createInvoice(req: Request, res: Response) {
const request = await got.get(`https://api.coingecko.com/api/v3/simple/price?ids=${cgFormat.join(',')}&vs_currencies=${currency.toLowerCase()}`, { const request = await got.get(`https://api.coingecko.com/api/v3/simple/price?ids=${cgFormat.join(',')}&vs_currencies=${currency.toLowerCase()}`, {
responseType: 'json' responseType: 'json'
}); });
console.log(request.body);
// Calulate total price, if cart is provided // Calulate total price, if cart is provided
if (cart !== undefined && totalPrice === undefined) { if (cart !== undefined && totalPrice === undefined) {
@@ -82,15 +81,13 @@ export async function createInvoice(req: Request, res: Response) {
let paymentMethods: IPaymentMethod[] = []; let paymentMethods: IPaymentMethod[] = [];
cgFormat.forEach(coinFullName => { cgFormat.forEach(coinFullName => {
console.log(coinFullName);
const coin = CryptoUnits[coinFullName.toUpperCase()]; const coin = CryptoUnits[coinFullName.toUpperCase()];
const exRate = Number(request.body[coinFullName][currency.toLowerCase()]);
paymentMethods.push({ method: coin, amount: paymentMethods.push({ exRate, method: coin, amount: roundNumber(totalPrice / exRate, decimalPlaces.get(coin))});
roundNumber(totalPrice / Number(request.body[coinFullName][currency.toLowerCase()]), decimalPlaces.get(coin))
});
}); });
const dueBy = new Date(Date.now() + 1000 * 60 * 15); const dueBy = new Date(Date.now() + 1000 * 60 * 15);
Invoice.create({ Invoice.create({
selector: randomString(128), selector: randomString(128),
@@ -103,7 +100,7 @@ export async function createInvoice(req: Request, res: Response) {
dueBy dueBy
}, (error, invoice: IInvoice) => { }, (error, invoice: IInvoice) => {
if (error) { if (error) {
res.status(500).send({error: error.message}); res.status(500).send({message: error.message});
return; return;
} }
@@ -219,7 +216,6 @@ export async function setPaymentMethod(req: Request, res: Response) {
res.status(404).send(); res.status(404).send();
return; return;
} }
invoice.status = PaymentStatus.PENDING; invoice.status = PaymentStatus.PENDING;
invoice.paymentMethod = CryptoUnits[findCryptoBySymbol(method)]; invoice.paymentMethod = CryptoUnits[findCryptoBySymbol(method)];

View File

@@ -79,7 +79,7 @@ export abstract class BackendProvider {
* *
* *Mainly used when LibrePay starts.* * *Mainly used when LibrePay starts.*
*/ */
abstract validateInvoices(invoices: IInvoice[]): void; abstract validateInvoice(invoices: IInvoice): void;
} }
export interface ITransactionDetails { export interface ITransactionDetails {

View File

@@ -16,16 +16,13 @@ export class InvoiceManager {
this.pendingInvoices = []; this.pendingInvoices = [];
this.knownConfirmations = new Map<string, number>(); this.knownConfirmations = new Map<string, number>();
// Get all pending transcations // Get all pending and unconfirmed transcations
Invoice.find({ status: PaymentStatus.PENDING }).then(invoices => { Invoice.find({ $or: [ { status: PaymentStatus.PENDING }, { status: PaymentStatus.UNCONFIRMED } ]}).then(invoices => {
logger.info(`There are ${invoices.length} invoices pending`); logger.info(`There are ${invoices.length} invoices that are pending or unconfirmed`);
providerManager.getProvider(CryptoUnits.BITCOIN).validateInvoices(invoices);
});
// Get all unconfirmed transactions invoices.forEach(invoice => {
Invoice.find({ status: PaymentStatus.UNCONFIRMED }).then(invoices => { providerManager.getProvider(invoice.paymentMethod).validateInvoice(invoice);
logger.info(`There are ${invoices.length} invoices unconfirmed`); });
providerManager.getProvider(CryptoUnits.BITCOIN).validateInvoices(invoices);
}); });
this.expireScheduler(); this.expireScheduler();
@@ -36,6 +33,7 @@ export class InvoiceManager {
*/ */
private expireScheduler() { private expireScheduler() {
setInterval(async () => { setInterval(async () => {
// Find invoices that are pending or requested and reached there EOF date
const expiredInvoices = await Invoice.find({ const expiredInvoices = await Invoice.find({
dueBy: { $lte: new Date() }, dueBy: { $lte: new Date() },
$or: [ { status: PaymentStatus.PENDING }, { status: PaymentStatus.REQUESTED } ] $or: [ { status: PaymentStatus.PENDING }, { status: PaymentStatus.REQUESTED } ]
@@ -180,7 +178,6 @@ export class InvoiceManager {
*/ */
logger.warning(`Transaction (${tx}) did not sent requested funds. (sent: ${receivedTranscation.amount} BTC, requested: ${price} BTC)`); logger.warning(`Transaction (${tx}) did not sent requested funds. (sent: ${receivedTranscation.amount} BTC, requested: ${price} BTC)`);
invoice.status = PaymentStatus.TOOLITTLE; invoice.status = PaymentStatus.TOOLITTLE;
this.removeInvoice(invoice);
await invoice.save(); await invoice.save();

View File

@@ -0,0 +1,12 @@
import { BackendProvider } from "../backendProvider";
import { CryptoUnits } from "../types";
export class Provider implements BackendProvider {
NAME = 'Bitcoin Blockchain';
DESCRIPTION = 'This provider queries the API backend provider by blockchain.com';
AUTHOR = 'LibrePay Team';
VERSION = '0.1'
CRYPTO = CryptoUnits.BITCOIN;
}

View File

@@ -1,9 +1,8 @@
import { Subscriber } from 'zeromq'; import { Subscriber } from 'zeromq';
import { config } from '../../../config';
import { invoiceManager, logger, rpcClient } from '../../app'; import { invoiceManager, logger, rpcClient } from '../../app';
import { IInvoice } from '../../models/invoice/invoice.interface'; import { IInvoice } from '../../models/invoice/invoice.interface';
import { BackendProvider, IRawTransaction, ITransaction, ITransactionDetails, ITransactionList } from '../backendProvider'; import { BackendProvider, IRawTransaction, ITransaction, ITransactionList } from '../backendProvider';
import { CryptoUnits, PaymentStatus } from '../types'; import { CryptoUnits, PaymentStatus } from '../types';
export class Provider implements BackendProvider { export class Provider implements BackendProvider {
@@ -78,8 +77,6 @@ export class Provider implements BackendProvider {
reject(err); reject(err);
return; return;
} }
console.log('sendToAddress:', decoded.result);
resolve(decoded.result.txid); resolve(decoded.result.txid);
}); });
@@ -120,30 +117,26 @@ export class Provider implements BackendProvider {
const transcation = invoice.transcationHash; const transcation = invoice.transcationHash;
const tx = await this.getTransaction(transcation); const tx = await this.getTransaction(transcation);
invoiceManager.setConfirmationCount(invoice, tx.confirmations); invoiceManager.setConfirmationCount(invoice, tx.confirmations);
}); });
}, 2_000); }, 2_000);
} }
async validateInvoices(invoices: IInvoice[]) { async validateInvoice(invoice: IInvoice) {
invoices.forEach(async invoice => { if (invoice.status === PaymentStatus.DONE || invoice.status === PaymentStatus.CANCELLED) return;
if (invoice.status === PaymentStatus.DONE || invoice.status === PaymentStatus.CANCELLED) return; if (invoice.paymentMethod !== CryptoUnits.BITCOIN) return;
if (invoice.paymentMethod !== CryptoUnits.BITCOIN) return;
rpcClient.request('listreceivedbyaddress', [0, false, false, invoice.receiveAddress], async (err, message) => { rpcClient.request('listreceivedbyaddress', [0, false, false, invoice.receiveAddress], async (err, message) => {
if (err) { if (err) {
logger.error(`There was an error while getting transcations of address ${invoice.receiveAddress}: ${err.message}`); logger.error(`There was an error while getting transcations of address ${invoice.receiveAddress}: ${err.message}`);
return; return;
} }
const res = message.result[0] as ITransactionList; const res = message.result[0] as ITransactionList;
if (res === undefined) return; if (res === undefined) return;
console.log(res); res.txids.forEach(async tx => {
invoiceManager.validatePayment(invoice, tx);
res.txids.forEach(async tx => {
invoiceManager.validatePayment(invoice, tx);
});
}); });
}); });
} }

View File

@@ -10,7 +10,8 @@ export interface ICart {
export interface IPaymentMethod { export interface IPaymentMethod {
method: CryptoUnits; method: CryptoUnits;
amount: number amount: number;
exRate: number;
} }
export interface IInvoice extends Document { export interface IInvoice extends Document {

View File

@@ -1,5 +1,5 @@
import { Schema } from 'mongoose'; import { Schema } from 'mongoose';
import { socketManager } from '../../app'; import { invoiceManager, socketManager } from '../../app';
import { CryptoUnits, FiatUnits, PaymentStatus } from '../../helper/types'; import { CryptoUnits, FiatUnits, PaymentStatus } from '../../helper/types';
import { ICart, IInvoice } from './invoice.interface'; import { ICart, IInvoice } from './invoice.interface';
@@ -14,7 +14,8 @@ const schemaCart = new Schema({
const schemaPaymentMethods = new Schema({ const schemaPaymentMethods = new Schema({
method: { type: String, enum: Object.values(CryptoUnits), required: true }, method: { type: String, enum: Object.values(CryptoUnits), required: true },
amount: { type: Number, required: false } amount: { type: Number, required: false },
exRate: { type: Number, required: true }, // Exchange rate at creation
}, { _id: false }); }, { _id: false });
const schemaInvoice = new Schema({ const schemaInvoice = new Schema({
@@ -60,6 +61,12 @@ schemaInvoice.post('validate', function (doc, next) {
function updateStatus(doc: IInvoice, next) { function updateStatus(doc: IInvoice, next) {
socketManager.emitInvoiceEvent(doc, 'status', doc.status); socketManager.emitInvoiceEvent(doc, 'status', doc.status);
// If a status has a negative value, then this invoice has failed.
if (doc.status < 0) {
invoiceManager.removeInvoice(doc);
}
next(); next();
} }

View File

@@ -1,13 +1,18 @@
import { Router } from "express"; import { Router } from "express";
import { createInvoice, getConfirmation, getInvoice, getPaymentMethods, setPaymentMethod } from "../controllers/invoice"; import { cancelInvoice, createInvoice, getConfirmation, getInvoice, getPaymentMethods, setPaymentMethod } from "../controllers/invoice";
import { MW_User } from "../controllers/user"; import { MW_User } from "../controllers/user";
const invoiceRouter = Router() const invoiceRouter = Router()
// Get general information
invoiceRouter.get('/paymentmethods', getPaymentMethods); invoiceRouter.get('/paymentmethods', getPaymentMethods);
// Actions related to specific invoices
invoiceRouter.get('/:selector', getInvoice); invoiceRouter.get('/:selector', getInvoice);
invoiceRouter.delete('/:selector', cancelInvoice);
invoiceRouter.get('/:selector/confirmation', getConfirmation); invoiceRouter.get('/:selector/confirmation', getConfirmation);
invoiceRouter.post('/:selector/setmethod', setPaymentMethod); invoiceRouter.post('/:selector/setmethod', setPaymentMethod);
invoiceRouter.get('/', MW_User, getInvoice); invoiceRouter.get('/', MW_User, getInvoice);
invoiceRouter.post('/', MW_User, createInvoice); invoiceRouter.post('/', MW_User, createInvoice);