From 0b3502d81f64e6670b8687acee00ac7971182f2a Mon Sep 17 00:00:00 2001 From: Mondei1 Date: Mon, 28 Dec 2020 19:04:13 +0100 Subject: [PATCH] New abstract structure Events for invoices get emitted in rooms --- Socket Events.md | 5 +- config.ts | 12 ++ src/app.ts | 11 +- src/controllers/invoice.ts | 93 +++++++------- src/helper/backendProvider.ts | 112 +++++++++++++++++ src/helper/invoiceManager.ts | 85 +++++++++++++ src/helper/invoiceScheduler.ts | 134 --------------------- src/helper/providerManager.ts | 20 +++ src/helper/providers/bitcoinCore.ts | 154 ++++++++++++++++++++++++ src/helper/socketio.ts | 27 +---- src/helper/types.ts | 5 + src/models/invoice/invoice.interface.ts | 21 ++-- src/models/invoice/invoice.schema.ts | 14 +-- src/routes/invoice.ts | 3 +- 14 files changed, 467 insertions(+), 229 deletions(-) create mode 100644 src/helper/backendProvider.ts create mode 100644 src/helper/invoiceManager.ts delete mode 100644 src/helper/invoiceScheduler.ts create mode 100644 src/helper/providerManager.ts create mode 100644 src/helper/providers/bitcoinCore.ts diff --git a/Socket Events.md b/Socket Events.md index 7eee41f..2b7dd2b 100644 --- a/Socket Events.md +++ b/Socket Events.md @@ -1,10 +1,7 @@ # Socket events ## Invoice status -### Requests -* `subscribe` - Subscribe to a invoices progress. Returns `true` if successful. - * `selector` - Your selector - +In order to receive updates about the a specific invoice, **you have to join the room with the selector.** ### Events * `status` - Status changed (see PaymentStatus enum) * `confirmationUpdate` - When there is a new confirmation on the transaction diff --git a/config.ts b/config.ts index 0654b82..255a56f 100644 --- a/config.ts +++ b/config.ts @@ -1,3 +1,4 @@ +import { CryptoUnits } from './src/helper/types'; /** * Here you can change various settings like database credentials, http settings and more. * @@ -18,6 +19,14 @@ export const config: IConfig = { transcations: { // If a payment has been made and its value is this amount less, it would be still accepted. acceptMargin: 0.00000001 + }, + payment: { + // This is a list of cryptocurrencies that you want to accpet. + methods: [ + CryptoUnits.BITCOIN, + CryptoUnits.DOGECOIN, + CryptoUnits.ETHEREUM + ] } } /** @@ -40,5 +49,8 @@ export interface IConfig { }, transcations: { acceptMargin: number + }, + payment: { + methods: CryptoUnits[]; } } \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index fefe65a..efea964 100644 --- a/src/app.ts +++ b/src/app.ts @@ -6,15 +6,17 @@ import * as rpc from 'jayson'; import * as mongoose from 'mongoose'; import * as winston from 'winston'; import * as socketio from 'socket.io'; +import { resolve } from 'path'; import { Server } from 'http'; import { config } from '../config'; import { hashPassword, randomPepper, randomString } from './helper/crypto'; -import { InvoiceScheduler } from './helper/invoiceScheduler'; +import { InvoiceManager } from './helper/invoiceManager'; import { User } from './models/user/user.model'; import { invoiceRouter } from './routes/invoice'; import { userRouter } from './routes/user'; import { SocketManager } from './helper/socketio'; +import { ProviderManager } from './helper/providerManager'; // Load .env dconfig({ debug: true, encoding: 'UTF-8' }); @@ -25,7 +27,7 @@ export const JWT_SECRET = process.env.JWT_SECRET || ""; export const INVOICE_SECRET = process.env.INVOICE_SECRET || ""; export let rpcClient: rpc.HttpClient | undefined = undefined; -export let invoiceScheduler: InvoiceScheduler | undefined = undefined; +export let invoiceScheduler: InvoiceManager | undefined = undefined; export let socketManager: SocketManager | undefined = undefined; export let logger: winston.Logger; @@ -106,7 +108,10 @@ async function run() { logger.debug("At least one admin user already exists, skip."); } - invoiceScheduler = new InvoiceScheduler(); + const providerManager = new ProviderManager(resolve('./src/helper/providers')); + providerManager.scan(); + + invoiceScheduler = new InvoiceManager(); const app = express(); const http = new Server(app); diff --git a/src/controllers/invoice.ts b/src/controllers/invoice.ts index 1aa44dd..df315cd 100644 --- a/src/controllers/invoice.ts +++ b/src/controllers/invoice.ts @@ -1,5 +1,6 @@ import { Request, Response } from 'express'; import got from 'got'; +import { config } from '../../config'; import { invoiceScheduler, INVOICE_SECRET, rpcClient } from '../app'; import { randomString } from '../helper/crypto'; @@ -62,53 +63,55 @@ export async function createInvoice(req: Request, res: Response) { return; } - rpcClient.request('getnewaddress', ['', 'bech32'], async (err, response) => { - if (err) throw err; + // Get price + // Convert coin symbol to full text in order to query Coin Gecko. eg.: ['btc', 'xmr'] => ['bitcoin', 'monero'] + let cgFormat = []; - // Get price - // Convert coin symbol to full text in order to query Coin Gecko. eg.: ['btc', 'xmr'] => ['bitcoin', 'monero'] - let cgFormat = []; - - paymentMethodsRaw.forEach(coin => { - const crypto = findCryptoBySymbol(coin); - - if (crypto !== undefined) { - cgFormat.push(crypto.toLowerCase()); - } - }); + paymentMethodsRaw.forEach(coin => { + const crypto = findCryptoBySymbol(coin); - const request = await got.get(`https://api.coingecko.com/api/v3/simple/price?ids=${cgFormat.join(',')}&vs_currencies=${currency.toLowerCase()}`, { - responseType: 'json' - }); + if (crypto !== undefined) { + cgFormat.push(crypto.toLowerCase()); + } + }); + + const request = await got.get(`https://api.coingecko.com/api/v3/simple/price?ids=${cgFormat.join(',')}&vs_currencies=${currency.toLowerCase()}`, { + responseType: 'json' + }); - // Calulate total price, if cart is provided - if (cart !== undefined && totalPrice === undefined) { - totalPrice = calculateCart(cart); + // Calulate total price, if cart is provided + if (cart !== undefined && totalPrice === undefined) { + totalPrice = calculateCart(cart); + } + + let paymentMethods: IPaymentMethod[] = []; + config.payment.methods.forEach(coin => { + paymentMethods.push({ method: CryptoUnits[coin.toUpperCase()], amount: totalPrice / Number(request.body[coin][currency.toLowerCase()]) }); + }); + + const dueBy = new Date(Date.now() + 1000 * 60 * 60); + + Invoice.create({ + selector: randomString(128), + paymentMethods, + successUrl, + cancelUrl, + cart, + currency, + totalPrice, + dueBy + }, (error, invoice: IInvoice) => { + if (error) { + res.status(500).send({error: error.message}); + return; } - let paymentMethods: IPaymentMethod[] = []; - Object.keys(request.body).forEach(coin => { - paymentMethods.push({ method: CryptoUnits[coin.toUpperCase()], amount: totalPrice / Number(request.body[coin][currency.toLowerCase()]) }); - }); - - Invoice.create({ - selector: randomString(128), - paymentMethods: paymentMethods, - successUrl, - cancelUrl, - cart, - currency, - totalPrice, - dueBy: 60, - receiveAddress: response.result - }, (error, invoice: IInvoice) => { - if (error) { - res.status(500).send({error: error.message}); - return; - } - - invoiceScheduler.addInvoice(invoice); - res.status(200).send({ id: invoice.selector }); + //invoiceScheduler.addInvoice(invoice); + //res.status(200).send({ id: invoice.selector }); + res.status(200).send({ + methods: paymentMethods, + selector: invoice.selector, + expireDate: invoice.dueBy }); }); @@ -167,7 +170,7 @@ export async function getInvoice(req: Request, res: Response) { } // DELETE /invoice/:selector -export async function cancelPaymnet(req: Request, res: Response) { +export async function cancelInvoice(req: Request, res: Response) { const selector = req.params.selector; // If an id is provided @@ -182,4 +185,8 @@ export async function cancelPaymnet(req: Request, res: Response) { await invoice.save(); return; } +} + +export async function getPaymentMethods(req: Request, res: Response) { + res.status(200).send({ methods: config.payment.methods }); } \ No newline at end of file diff --git a/src/helper/backendProvider.ts b/src/helper/backendProvider.ts new file mode 100644 index 0000000..e2c1d31 --- /dev/null +++ b/src/helper/backendProvider.ts @@ -0,0 +1,112 @@ +import { InvoiceManager } from './invoiceManager'; +import { CryptoUnits } from './types'; + +/** + * This backend provider class is required to write your own backends. + * + * *By default LibrePay supports Bitcoin Core.* + */ +export abstract class BackendProvider { + + invoiceManager: InvoiceManager = null; + + constructor (invoiceManager: InvoiceManager) { + this.invoiceManager = invoiceManager; + } + + /* Provider information */ + abstract readonly NAME: string; + abstract readonly DESCRIPTION: string; + abstract readonly VERSION: string; + abstract readonly AUTHOR: string; + + /** + * The cryptocurrency that this providers supports. + */ + abstract readonly CRYPTO: CryptoUnits; + + /** + * This function gets called when this provider gets activated. + */ + abstract onEnable(): void; + + /** + * Generate a new address to receive new funds. + */ + abstract getNewAddress(): Promise; + + /** + * Get a transaction from the blockchain. + * @param txId Hash of the transcation you're looking for. + * @returns See https://developer.bitcoin.org/reference/rpc/gettransaction.html for reference + */ + abstract getTransaction(txId: string): Promise; + + /** + * Decode a raw transcation that was broadcasted in the network. + * @param rawTx Raw transcation + * @returns See https://developer.bitcoin.org/reference/rpc/decoderawtransaction.html for reference + */ + abstract decodeRawTransaction(rawTx: string): Promise; + + /** + * Send funds to a specific address. + * @param recipient Address of the recipient + * @param amount Amount of coins to transfer + * @param comment Comment what this transaction is about + * @param commentTo Comment on who is receiving it + * @param subtractFeeFromAmount The fee will be deducted from the amount being sent + * @returns The transcation id + */ + abstract sendToAddress( + recipient: string, + amount: number, + comment?: string, + commentTo?: string, + subtractFeeFromAmount?: boolean): Promise; + + /** + * Wait for new transactions by the network. + */ + abstract listener(): void; + + /** + * Keep track of unconfirmed transactions. + */ + abstract watchConfirmations(): void; +} + +export interface ITransaction { + amount: number; + fee: number; + confirmations: number; + time: number; // Unix timestamp + details: { + address: string; + category: 'send' | 'receive' | 'generate' | 'immature' | 'orphan' + vout: number; + fee: number; + abandoned: boolean + }[]; + hex: string; +} + +export interface IRawTransaction { + txid: string; + hash: string; + size: number; + vsize: number; + weight: number; + version: number; + vin: { + txid: string; + vout: number; + }[]; + vout: { + value: number; + n: number; + scriptPubKey: { + addresses: string[]; + } + }[]; +} \ No newline at end of file diff --git a/src/helper/invoiceManager.ts b/src/helper/invoiceManager.ts new file mode 100644 index 0000000..aa68b58 --- /dev/null +++ b/src/helper/invoiceManager.ts @@ -0,0 +1,85 @@ +import { IInvoice } from "../models/invoice/invoice.interface"; +import { Subscriber } from 'zeromq'; +import { logger, rpcClient, socketManager } from "../app"; +import { invoiceRouter } from "../routes/invoice"; +import { Invoice } from "../models/invoice/invoice.model"; +import { CryptoUnits, PaymentStatus } from "./types"; +import { config } from "../../config"; + +/** + * This invoice manager keeps track of the status of each transaction. + */ +export class InvoiceManager { + private pendingInvoices: IInvoice[]; + private unconfirmedTranscations: IInvoice[]; + private knownConfirmations: Map; // Invoice id / confirmation count + + constructor() { + this.unconfirmedTranscations = []; + this.pendingInvoices = []; + this.knownConfirmations = new Map(); + + // Get all pending transcations + Invoice.find({ status: PaymentStatus.PENDING }).then(invoices => { + this.pendingInvoices = invoices; + }); + + // Get all unconfirmed transactions + Invoice.find({ status: PaymentStatus.UNCONFIRMED }).then(invoices => { + this.unconfirmedTranscations = invoices; + }); + + this.watchConfirmations(); + } + + /** + * This will add `invoice` to the pending list. + */ + addInvoice(invoice: IInvoice) { + logger.info(`A new invoice has been created: ${invoice.id}`) + this.pendingInvoices.push(invoice); + } + + removeInvoice(invoice: IInvoice) { + this.unconfirmedTranscations.splice(this.unconfirmedTranscations.indexOf(invoice), 1); + } + + /** + * Upgrade a pending invoice up to an unconfirmed invoice. + */ + upgradeInvoice(invoice: IInvoice) { + const target = this.pendingInvoices.find(item => { return item.id = invoice.id }); + if (target !== undefined) { + this.pendingInvoices.push(invoice); + this.pendingInvoices.splice(this.pendingInvoices.indexOf(invoice), 1); + } + } + + getPendingInvoices() { + return this.pendingInvoices; + } + + getUnconfirmedTransactions() { + return this.unconfirmedTranscations; + } + + hasConfirmationChanged(invoice: IInvoice, confirmations: number) { + return this.knownConfirmations.get(invoice.id) !== confirmations; + } + + getConfirmationCount(invoice: IInvoice) { + return this.knownConfirmations.get(invoice.id); + } + + setConfirmationCount(invoice: IInvoice, count: number) { + socketManager.emitInvoiceEvent(invoice, 'confirmationUpdate', { count }); + return this.knownConfirmations.set(invoice.id, count); + } + + /** + * This functions loops over each unconfirmed transaction to check if it reached "trusted" threshold. + */ + private watchConfirmations() { + + } +} \ No newline at end of file diff --git a/src/helper/invoiceScheduler.ts b/src/helper/invoiceScheduler.ts deleted file mode 100644 index fc71c89..0000000 --- a/src/helper/invoiceScheduler.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { IInvoice } from "../models/invoice/invoice.interface"; -import { Subscriber } from 'zeromq'; -import { logger, rpcClient, socketManager } from "../app"; -import { invoiceRouter } from "../routes/invoice"; -import { Invoice } from "../models/invoice/invoice.model"; -import { CryptoUnits, PaymentStatus } from "./types"; -import { config } from "../../config"; - -export class InvoiceScheduler { - private pendingInvoices: IInvoice[]; - private unconfirmedTranscations: IInvoice[]; - private knownConfirmations: Map; // Invoice id / confirmation cound - private sock: Subscriber; - - constructor() { - this.unconfirmedTranscations = []; - this.pendingInvoices = []; - this.knownConfirmations = new Map(); - - // Get all pending transcations - Invoice.find({ status: PaymentStatus.PENDING }).then(invoices => { - this.pendingInvoices = invoices; - }); - - // Get all unconfirmed transactions - Invoice.find({ status: PaymentStatus.UNCONFIRMED }).then(invoices => { - this.unconfirmedTranscations = invoices; - }); - - this.sock = new Subscriber(); - this.sock.connect('tcp://127.0.0.1:29000'); - this.listen(); - this.watchConfirmations(); - } - - addInvoice(invoice: IInvoice) { - logger.info(`A new invoice has been created: ${invoice.id}`) - this.pendingInvoices.push(invoice); - } - - /** - * This function waits for Bitcoin Core to respond with raw TX. - */ - private async listen() { - this.sock.subscribe('rawtx'); - - logger.info('Now listing for incoming transaction to any invoices ...'); - for await (const [topic, msg] of this.sock) { - const rawtx = msg.toString('hex'); - //logger.debug(`New tx: ${rawtx}`); - rpcClient.request('decoderawtransaction', [rawtx], (err, decoded) => { - if (err) { - logger.error(`Error while decoding raw tx: ${err.message}`); - return; - } - - decoded.result.vout.forEach(output => { - // Loop over each output and check if the address of one matches the one of an invoice. - this.pendingInvoices.forEach(invoice => { - if (output.scriptPubKey.addresses === undefined) return; // Sometimes (weird) transaction don't have any addresses - - // We found our transaction (https://developer.bitcoin.org/reference/rpc/decoderawtransaction.html) - if (output.scriptPubKey.addresses.indexOf(invoice.receiveAddress) !== -1) { - invoice.paid += output.value; - logger.info(`Transcation for invoice ${invoice.id} received! (${decoded.result.hash})`); - - // Change state in database - const price = invoice.paymentMethods.find((item) => { return item.method === CryptoUnits.BITCOIN }).amount; - if (invoice.paid < price - config.transcations.acceptMargin) { - const left = price - output.value; - invoice.status = PaymentStatus.PARTIALLY; - invoice.save(); - logger.info(`Transcation for invoice ${invoice.id} received but there are still ${left} BTC missing (${decoded.result.hash})`); - } else { - invoice.status = PaymentStatus.UNCONFIRMED; - invoice.transcationHashes.push(decoded.result.txid); - invoice.save(); - - // Push to array & remove from pending - this.unconfirmedTranscations.push(invoice); - this.pendingInvoices.splice(this.pendingInvoices.indexOf(invoice), 1); - } - } - }) - }); - }); - - } - } - - /** - * This functions loops over each unconfirmed transaction to check if it reached "trusted" threshold. - */ - private watchConfirmations() { - setInterval(() => { - this.unconfirmedTranscations.forEach(invoice => { - if (invoice.transcationHashes.length === 0) return; - let trustworthy = true; // Will be true if all transactions are above threshold. - - for (let i = 0; i < invoice.transcationHashes.length; i++) { - const transcation = invoice.transcationHashes[i]; - - rpcClient.request('gettransaction', [transcation], (err, message) => { - if (err) { - logger.error(`Error while fetching confirmation state of ${transcation}: ${err.message}`); - trustworthy = false; - - return; - } - - if (this.knownConfirmations.get(invoice.id) != message.result.confirmations) { - this.knownConfirmations.set(invoice.id, message.result.confirmations); - socketManager.getSocketByInvoice(invoice).emit('confirmationUpdate', { count: Number(message.result.confirmations) }); - } - - if (Number(message.result.confirmations) > 0) { - logger.info(`Transaction (${transcation}) has reached more then 2 confirmations and can now be trusted!`); - - this.unconfirmedTranscations.splice(this.unconfirmedTranscations.indexOf(invoice), 1); - } else { - trustworthy = false; - logger.debug(`Transcation (${transcation}) has not reached his threshold yet.`); - } - }); - } - - if (trustworthy) { - invoice.status = PaymentStatus.DONE; - invoice.save(); // This will trigger a post save hook that will notify the user. - } - }); - }, 2_000); - } -} \ No newline at end of file diff --git a/src/helper/providerManager.ts b/src/helper/providerManager.ts new file mode 100644 index 0000000..e39fafd --- /dev/null +++ b/src/helper/providerManager.ts @@ -0,0 +1,20 @@ +import { readdirSync } from 'fs'; + +export class ProviderManager { + + providerFilePath: string; + + constructor(filePath: string) { + this.providerFilePath = filePath; + } + + scan() { + const getDirectories = () => + readdirSync(this.providerFilePath, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + .map(dirent => dirent.name) + + console.log(getDirectories()); + } + +} \ No newline at end of file diff --git a/src/helper/providers/bitcoinCore.ts b/src/helper/providers/bitcoinCore.ts new file mode 100644 index 0000000..d23f439 --- /dev/null +++ b/src/helper/providers/bitcoinCore.ts @@ -0,0 +1,154 @@ +import { Socket, Subscriber } from "zeromq"; +import { config } from "../../../config"; +import { logger, rpcClient } from "../../app"; +import { BackendProvider, ITransaction, IRawTransaction } from "../backendProvider"; +import { InvoiceManager } from "../invoiceManager"; +import { CryptoUnits, PaymentStatus } from "../types"; + +export class BitcoinCore implements BackendProvider { + invoiceManager: InvoiceManager; + + private sock: Subscriber; + + NAME = 'Bitcoin Core'; + DESCRIPTION = 'This provider communicates with the Bitcoin Core application.'; + AUTHOR = 'LibrePay Team'; + VERSION = '0.1'; + CRYPTO = CryptoUnits.BITCOIN; + + onEnable() { + logger.info('The Bitcoin Core backend is now available!'); + } + + async getNewAddress(): Promise { + return new Promise((resolve, reject) => { + rpcClient.request('getnewaddress', ['', 'bech32'], async (err, message) => { + if (err) { + reject(err); + return; + } + + resolve(message.result); + }); + }); + } + + async getTransaction(txId: string): Promise { + return new Promise((resolve, reject) => { + rpcClient.request('gettransaction', [txId], (err, message) => { + if (err) { + reject(err); + return; + } + + resolve(message.result); + }); + }); + } + + async decodeRawTransaction(rawTx: string): Promise { + return new Promise((resolve, reject) => { + rpcClient.request('decoderawtransaction', [rawTx], (err, decoded) => { + if (err) { + reject(err); + return; + } + + resolve(decoded.result); + }); + }); + } + + async sendToAddress( + recipient: string, + amount: number, + comment?: string, + commentTo?: string, + subtractFeeFromAmount?: boolean): Promise { + return new Promise((resolve, reject) => { + rpcClient.request('sendtoaddress', [recipient, amount, comment, commentTo, subtractFeeFromAmount], (err, decoded) => { + if (err) { + reject(err); + return; + } + + resolve(decoded.result.txid); + }); + }); + } + + async listener() { + this.sock = new Subscriber(); + this.sock.connect('tcp://127.0.0.1:29000'); + this.sock.subscribe('rawtx'); + + logger.info('Now listing for incoming transaction to any invoices ...'); + for await (const [topic, msg] of this.sock) { + const rawtx = msg.toString('hex'); + const tx = await this.decodeRawTransaction(rawtx); + + tx.vout.forEach(output => { + // Loop over each output and check if the address of one matches the one of an invoice. + this.invoiceManager.getPendingInvoices().forEach(async invoice => { + if (output.scriptPubKey.addresses === undefined) return; // Sometimes (weird) transaction don't have any addresses + + // We found our transaction (https://developer.bitcoin.org/reference/rpc/decoderawtransaction.html) + if (output.scriptPubKey.addresses.indexOf(invoice.receiveAddress) !== -1) { + const senderAddress = output.scriptPubKey.addresses[output.scriptPubKey.addresses.indexOf(invoice.receiveAddress)]; + logger.info(`Transcation for invoice ${invoice.id} received! (${tx.hash})`); + + // Change state in database + const price = invoice.paymentMethods.find((item) => { return item.method === CryptoUnits.BITCOIN }).amount; + if (output.value < price - config.transcations.acceptMargin) { + const left = price - output.value; + logger.info(`Transcation for invoice ${invoice.id} received but there are ${left} BTC missing (${tx.hash}).`); + + const txBack = await this.sendToAddress(senderAddress, output.value, null, null, true); + logger.info(`Sent ${output.value} BTC back to ${senderAddress}`); + } else { + invoice.status = PaymentStatus.UNCONFIRMED; + invoice.transcationHashes = tx.txid; + invoice.save(); + + this.invoiceManager.upgradeInvoice(invoice); + } + } + }) + }); + + } + } + + async watchConfirmations() { + setInterval(() => { + this.invoiceManager.getUnconfirmedTransactions().forEach(async invoice => { + if (invoice.transcationHashes.length === 0) return; + let trustworthy = true; // Will be true if all transactions are above threshold. + + for (let i = 0; i < invoice.transcationHashes.length; i++) { + const transcation = invoice.transcationHashes[i]; + + const tx = await this.getTransaction(transcation); + + if (this.invoiceManager.hasConfirmationChanged(invoice, tx.confirmations)) { + this.invoiceManager.setConfirmationCount(invoice, tx.confirmations); + } + + if (Number(tx.confirmations) > 0) { + logger.info(`Transaction (${transcation}) has reached more then 2 confirmations and can now be trusted!`); + this.invoiceManager.removeInvoice(invoice); + } else { + trustworthy = false; + logger.debug(`Transcation (${transcation}) has not reached his threshold yet.`); + } + } + + if (trustworthy) { + invoice.status = PaymentStatus.DONE; + invoice.save(); // This will trigger a post save hook that will notify the user. + } + }); + }, 2_000); + } +} + diff --git a/src/helper/socketio.ts b/src/helper/socketio.ts index 75d14b8..614921a 100644 --- a/src/helper/socketio.ts +++ b/src/helper/socketio.ts @@ -6,16 +6,9 @@ import { PaymentStatus } from "./types"; export class SocketManager { io: Server; - - private socketInvoice: Map; // Socket ID / _id - private idSocket: Map; // Socket ID / Socket - private invoiceSocket: Map; // _id / Socket constructor(io: Server) { this.io = io; - this.socketInvoice = new Map(); - this.idSocket = new Map(); - this.invoiceSocket = new Map(); this.listen(); } @@ -23,8 +16,6 @@ export class SocketManager { console.log("Listen"); this.io.on('connection', (socket: Socket) => { - this.idSocket.set(socket.id, socket); - // The frontend sends his selector, then pick _id and put it in `socketInvoice` map. // Return `true` if successful and `false` if not. socket.on('subscribe', async data => { @@ -36,25 +27,13 @@ export class SocketManager { } logger.info(`Socket ${socket.id} has subscribed to invoice ${invoice.id} (${PaymentStatus[invoice.status]})`); - - this.socketInvoice.set(socket.id, invoice.id); - this.invoiceSocket.set(invoice.id, socket); - socket.emit('subscribe', true); } }); }); } - getSocketById(id: string) { - return this.idSocket.get(id); - } - - async getInvoiceBySocket(socketId: string) { - const invoiceId = this.socketInvoice.get(socketId); - return await Invoice.findById(invoiceId); - } - - getSocketByInvoice(invoice: IInvoice) { - return this.invoiceSocket.get(invoice.id); + emitInvoiceEvent(invoice: IInvoice, event: string, data: any) { + logger.debug(`Broadcast ${data} to room ${invoice.selector}`); + this.io.to(invoice.selector).emit(event, data); } } \ No newline at end of file diff --git a/src/helper/types.ts b/src/helper/types.ts index 7d31938..00ae3e5 100644 --- a/src/helper/types.ts +++ b/src/helper/types.ts @@ -20,6 +20,11 @@ export enum FiatUnits { } export enum PaymentStatus { + /** + * The invoice has been requested but the payment method has to be choosen. + */ + REQUESTED = -1, + /** * The payment has not been yet started. The user did not initiated the transfer. */ diff --git a/src/models/invoice/invoice.interface.ts b/src/models/invoice/invoice.interface.ts index f9bb620..3e76eaa 100644 --- a/src/models/invoice/invoice.interface.ts +++ b/src/models/invoice/invoice.interface.ts @@ -17,29 +17,26 @@ export interface IInvoice extends Document { selector: string; // Available payment methods - // [{ method: 'btc', amount: 0.0000105 }] + // { method: 'btc', amount: 0.0000105 } paymentMethods: IPaymentMethod[]; + // This is the method choosen by the user + paymentMethod?: CryptoUnits; + + // Will be created as soon as the user picked one options // 1Kss3e9iPB9vTgWJJZ1SZNkkFKcFJXPz9t - receiveAddress: string; - - paidWith?: CryptoUnits; - - // Already paid amount, in case that not the entire amount was paid with once. - // 0.000013 - paid?: number; + receiveAddress?: string; // Is set when invoice got paid // 3b38c3a215d4e7981e1516b2dcbf76fca58911274d5d55b3d615274d6e10f2c1 - transcationHashes?: string[]; + transcationHashes?: string; cart?: ICart[]; totalPrice?: number; currency: FiatUnits; - // Time in minutes the user has to pay. - // Time left = (createdAt + dueBy) - Date.now() / 1000 - dueBy: number; + // Datetime the user has to pay. + dueBy: Date; status?: PaymentStatus; diff --git a/src/models/invoice/invoice.schema.ts b/src/models/invoice/invoice.schema.ts index d6128c9..7b4703d 100644 --- a/src/models/invoice/invoice.schema.ts +++ b/src/models/invoice/invoice.schema.ts @@ -20,15 +20,14 @@ const schemaPaymentMethods = new Schema({ const schemaInvoice = new Schema({ selector: { type: String, length: 128, required: true }, paymentMethods: [{ type: schemaPaymentMethods, required: true }], - receiveAddress: { type: String, required: true }, - paidWith: { type: String, enum: CryptoUnits }, - paid: { type: Number, default: 0 }, - transcationHashes: [{ type: String, required: false }], + paymentMethod: { type: String, enum: Object.values(CryptoUnits), required: false }, + receiveAddress: { type: String, required: false }, + transcationHashes: { type: String, required: false }, cart: [{ type: schemaCart, required: false }], totalPrice: { type: Number, required: false }, currency: { type: String, enum: Object.values(FiatUnits), required: true }, - dueBy: { type: Number, required: true }, - status: { type: Number, enum: Object.values(PaymentStatus), default: PaymentStatus.PENDING }, + dueBy: { type: Date, required: true }, + status: { type: Number, enum: Object.values(PaymentStatus), default: PaymentStatus.REQUESTED }, email: { type: String, required: false }, successUrl: { type: String, match: urlRegex, required: false }, cancelUrl: { type: String, match: urlRegex, required: false } @@ -62,8 +61,7 @@ schemaInvoice.post('validate', function (doc, next) { schemaInvoice.post('save', function(doc, next) { let self = this as IInvoice; - if (socketManager.getSocketByInvoice(self) === undefined) return; - socketManager.getSocketByInvoice(self).emit('status', self.status); + socketManager.emitInvoiceEvent(self, 'status', self.status); next(); }) diff --git a/src/routes/invoice.ts b/src/routes/invoice.ts index 3daf144..e4fca3c 100644 --- a/src/routes/invoice.ts +++ b/src/routes/invoice.ts @@ -1,9 +1,10 @@ import { Router } from "express"; -import { createInvoice, getInvoice } from "../controllers/invoice"; +import { createInvoice, getInvoice, getPaymentMethods } from "../controllers/invoice"; import { MW_User } from "../controllers/user"; const invoiceRouter = Router() +invoiceRouter.get('/paymentmethods', getPaymentMethods); invoiceRouter.get('/:selector', getInvoice); invoiceRouter.get('/', MW_User, getInvoice); invoiceRouter.post('/', MW_User, createInvoice);