From fc71aed6609d723c39d8ef3b0a6c08f4fb70fa9f Mon Sep 17 00:00:00 2001 From: Mondei1 Date: Sat, 2 Jan 2021 22:14:59 +0100 Subject: [PATCH] Save exchange rate with invoice - Fix issue where completed invoices were flagged as expired --- src/controllers/invoice.ts | 12 +++----- src/helper/backendProvider.ts | 2 +- src/helper/invoiceManager.ts | 17 +++++------ src/helper/providers/bitcoinBlockchain.js | 12 ++++++++ src/helper/providers/bitcoinCore.ts | 35 +++++++++-------------- src/models/invoice/invoice.interface.ts | 3 +- src/models/invoice/invoice.schema.ts | 11 +++++-- src/routes/invoice.ts | 7 ++++- 8 files changed, 55 insertions(+), 44 deletions(-) create mode 100644 src/helper/providers/bitcoinBlockchain.js diff --git a/src/controllers/invoice.ts b/src/controllers/invoice.ts index 6ec3e6f..04b70f7 100644 --- a/src/controllers/invoice.ts +++ b/src/controllers/invoice.ts @@ -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()}`, { responseType: 'json' }); - console.log(request.body); // Calulate total price, if cart is provided if (cart !== undefined && totalPrice === undefined) { @@ -82,15 +81,13 @@ export async function createInvoice(req: Request, res: Response) { let paymentMethods: IPaymentMethod[] = []; cgFormat.forEach(coinFullName => { - console.log(coinFullName); const coin = CryptoUnits[coinFullName.toUpperCase()]; + const exRate = Number(request.body[coinFullName][currency.toLowerCase()]); - paymentMethods.push({ method: coin, amount: - roundNumber(totalPrice / Number(request.body[coinFullName][currency.toLowerCase()]), decimalPlaces.get(coin)) - }); + paymentMethods.push({ exRate, method: coin, amount: roundNumber(totalPrice / exRate, decimalPlaces.get(coin))}); }); - const dueBy = new Date(Date.now() + 1000 * 60 * 15); + const dueBy = new Date(Date.now() + 1000 * 60 * 15); Invoice.create({ selector: randomString(128), @@ -103,7 +100,7 @@ export async function createInvoice(req: Request, res: Response) { dueBy }, (error, invoice: IInvoice) => { if (error) { - res.status(500).send({error: error.message}); + res.status(500).send({message: error.message}); return; } @@ -219,7 +216,6 @@ export async function setPaymentMethod(req: Request, res: Response) { res.status(404).send(); return; } - invoice.status = PaymentStatus.PENDING; invoice.paymentMethod = CryptoUnits[findCryptoBySymbol(method)]; diff --git a/src/helper/backendProvider.ts b/src/helper/backendProvider.ts index 64fe3d8..2444c26 100644 --- a/src/helper/backendProvider.ts +++ b/src/helper/backendProvider.ts @@ -79,7 +79,7 @@ export abstract class BackendProvider { * * *Mainly used when LibrePay starts.* */ - abstract validateInvoices(invoices: IInvoice[]): void; + abstract validateInvoice(invoices: IInvoice): void; } export interface ITransactionDetails { diff --git a/src/helper/invoiceManager.ts b/src/helper/invoiceManager.ts index 7c71faa..0d77d32 100644 --- a/src/helper/invoiceManager.ts +++ b/src/helper/invoiceManager.ts @@ -16,16 +16,13 @@ export class InvoiceManager { this.pendingInvoices = []; this.knownConfirmations = new Map(); - // Get all pending transcations - Invoice.find({ status: PaymentStatus.PENDING }).then(invoices => { - logger.info(`There are ${invoices.length} invoices pending`); - providerManager.getProvider(CryptoUnits.BITCOIN).validateInvoices(invoices); - }); + // Get all pending and unconfirmed transcations + Invoice.find({ $or: [ { status: PaymentStatus.PENDING }, { status: PaymentStatus.UNCONFIRMED } ]}).then(invoices => { + logger.info(`There are ${invoices.length} invoices that are pending or unconfirmed`); - // Get all unconfirmed transactions - Invoice.find({ status: PaymentStatus.UNCONFIRMED }).then(invoices => { - logger.info(`There are ${invoices.length} invoices unconfirmed`); - providerManager.getProvider(CryptoUnits.BITCOIN).validateInvoices(invoices); + invoices.forEach(invoice => { + providerManager.getProvider(invoice.paymentMethod).validateInvoice(invoice); + }); }); this.expireScheduler(); @@ -36,6 +33,7 @@ export class InvoiceManager { */ private expireScheduler() { setInterval(async () => { + // Find invoices that are pending or requested and reached there EOF date const expiredInvoices = await Invoice.find({ dueBy: { $lte: new Date() }, $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)`); invoice.status = PaymentStatus.TOOLITTLE; - this.removeInvoice(invoice); await invoice.save(); diff --git a/src/helper/providers/bitcoinBlockchain.js b/src/helper/providers/bitcoinBlockchain.js new file mode 100644 index 0000000..7e1a708 --- /dev/null +++ b/src/helper/providers/bitcoinBlockchain.js @@ -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; + +} \ No newline at end of file diff --git a/src/helper/providers/bitcoinCore.ts b/src/helper/providers/bitcoinCore.ts index ba10cef..6016508 100644 --- a/src/helper/providers/bitcoinCore.ts +++ b/src/helper/providers/bitcoinCore.ts @@ -1,9 +1,8 @@ import { Subscriber } from 'zeromq'; -import { config } from '../../../config'; import { invoiceManager, logger, rpcClient } from '../../app'; 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'; export class Provider implements BackendProvider { @@ -78,8 +77,6 @@ export class Provider implements BackendProvider { reject(err); return; } - - console.log('sendToAddress:', decoded.result); resolve(decoded.result.txid); }); @@ -120,30 +117,26 @@ export class Provider implements BackendProvider { const transcation = invoice.transcationHash; const tx = await this.getTransaction(transcation); - invoiceManager.setConfirmationCount(invoice, tx.confirmations); + invoiceManager.setConfirmationCount(invoice, tx.confirmations); }); }, 2_000); } - async validateInvoices(invoices: IInvoice[]) { - invoices.forEach(async invoice => { - if (invoice.status === PaymentStatus.DONE || invoice.status === PaymentStatus.CANCELLED) return; - if (invoice.paymentMethod !== CryptoUnits.BITCOIN) return; + async validateInvoice(invoice: IInvoice) { + if (invoice.status === PaymentStatus.DONE || invoice.status === PaymentStatus.CANCELLED) return; + if (invoice.paymentMethod !== CryptoUnits.BITCOIN) return; - rpcClient.request('listreceivedbyaddress', [0, false, false, invoice.receiveAddress], async (err, message) => { - if (err) { - logger.error(`There was an error while getting transcations of address ${invoice.receiveAddress}: ${err.message}`); - return; - } + rpcClient.request('listreceivedbyaddress', [0, false, false, invoice.receiveAddress], async (err, message) => { + if (err) { + logger.error(`There was an error while getting transcations of address ${invoice.receiveAddress}: ${err.message}`); + return; + } - const res = message.result[0] as ITransactionList; - if (res === undefined) return; + const res = message.result[0] as ITransactionList; + 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); }); }); } diff --git a/src/models/invoice/invoice.interface.ts b/src/models/invoice/invoice.interface.ts index 9b38294..a8ddcc9 100644 --- a/src/models/invoice/invoice.interface.ts +++ b/src/models/invoice/invoice.interface.ts @@ -10,7 +10,8 @@ export interface ICart { export interface IPaymentMethod { method: CryptoUnits; - amount: number + amount: number; + exRate: number; } export interface IInvoice extends Document { diff --git a/src/models/invoice/invoice.schema.ts b/src/models/invoice/invoice.schema.ts index 18edc77..161c48a 100644 --- a/src/models/invoice/invoice.schema.ts +++ b/src/models/invoice/invoice.schema.ts @@ -1,5 +1,5 @@ import { Schema } from 'mongoose'; -import { socketManager } from '../../app'; +import { invoiceManager, socketManager } from '../../app'; import { CryptoUnits, FiatUnits, PaymentStatus } from '../../helper/types'; import { ICart, IInvoice } from './invoice.interface'; @@ -14,7 +14,8 @@ const schemaCart = new Schema({ const schemaPaymentMethods = new Schema({ 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 }); const schemaInvoice = new Schema({ @@ -60,6 +61,12 @@ schemaInvoice.post('validate', function (doc, next) { function updateStatus(doc: IInvoice, next) { 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(); } diff --git a/src/routes/invoice.ts b/src/routes/invoice.ts index 69446af..7d019b3 100644 --- a/src/routes/invoice.ts +++ b/src/routes/invoice.ts @@ -1,13 +1,18 @@ 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"; const invoiceRouter = Router() +// Get general information invoiceRouter.get('/paymentmethods', getPaymentMethods); + +// Actions related to specific invoices invoiceRouter.get('/:selector', getInvoice); +invoiceRouter.delete('/:selector', cancelInvoice); invoiceRouter.get('/:selector/confirmation', getConfirmation); invoiceRouter.post('/:selector/setmethod', setPaymentMethod); + invoiceRouter.get('/', MW_User, getInvoice); invoiceRouter.post('/', MW_User, createInvoice);